@htlkg/core 0.0.1 → 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 (45) hide show
  1. package/README.md +51 -0
  2. package/dist/amplify-astro-adapter/index.d.ts +109 -0
  3. package/dist/amplify-astro-adapter/index.js +295 -0
  4. package/dist/amplify-astro-adapter/index.js.map +1 -0
  5. package/dist/auth/index.d.ts +1 -1
  6. package/dist/auth/index.js +305 -1
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/index.d.ts +220 -0
  9. package/dist/index.js +426 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/logger-BTW3fOeM.d.ts +45 -0
  12. package/dist/utils/index.d.ts +3 -0
  13. package/dist/utils/index.js +55 -0
  14. package/dist/utils/index.js.map +1 -1
  15. package/package.json +56 -33
  16. package/src/amplify-astro-adapter/amplify-astro-adapter.md +167 -0
  17. package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.test.ts +296 -0
  18. package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.ts +97 -0
  19. package/src/amplify-astro-adapter/createRunWithAmplifyServerContext.ts +84 -0
  20. package/src/amplify-astro-adapter/errors.test.ts +115 -0
  21. package/src/amplify-astro-adapter/errors.ts +105 -0
  22. package/src/amplify-astro-adapter/globalSettings.test.ts +78 -0
  23. package/src/amplify-astro-adapter/globalSettings.ts +16 -0
  24. package/src/amplify-astro-adapter/index.ts +14 -0
  25. package/src/amplify-astro-adapter/types.ts +55 -0
  26. package/src/auth/auth.md +178 -0
  27. package/src/auth/index.test.ts +180 -0
  28. package/src/auth/index.ts +294 -0
  29. package/src/constants/constants.md +132 -0
  30. package/src/constants/index.test.ts +116 -0
  31. package/src/constants/index.ts +98 -0
  32. package/src/core-exports.property.test.ts +186 -0
  33. package/src/errors/errors.md +177 -0
  34. package/src/errors/index.test.ts +153 -0
  35. package/src/errors/index.ts +134 -0
  36. package/src/index.ts +65 -0
  37. package/src/routes/index.ts +225 -0
  38. package/src/routes/routes.md +189 -0
  39. package/src/types/index.ts +94 -0
  40. package/src/types/types.md +144 -0
  41. package/src/utils/index.test.ts +257 -0
  42. package/src/utils/index.ts +112 -0
  43. package/src/utils/logger.ts +88 -0
  44. package/src/utils/utils.md +199 -0
  45. package/src/workspace.property.test.ts +235 -0
@@ -0,0 +1,178 @@
1
+ # Auth Module
2
+
3
+ Core authentication functions for server-side and client-side user management with AWS Cognito.
4
+
5
+ ## Installation
6
+
7
+ ```typescript
8
+ import {
9
+ getUser,
10
+ getClientUser,
11
+ hasAccessToBrand,
12
+ hasAccessToAccount,
13
+ isAdminUser,
14
+ isSuperAdminUser,
15
+ } from '@htlkg/core/auth';
16
+ ```
17
+
18
+ ## User Interface
19
+
20
+ ```typescript
21
+ interface User {
22
+ username: string;
23
+ email: string;
24
+ brandIds: number[];
25
+ accountIds: number[];
26
+ isAdmin: boolean;
27
+ isSuperAdmin: boolean;
28
+ roles: string[];
29
+ }
30
+ ```
31
+
32
+ ## Server-Side Authentication
33
+
34
+ ### getUser
35
+
36
+ Retrieves the authenticated user from Astro's API context using Cognito cookies.
37
+
38
+ ```typescript
39
+ import type { APIContext } from 'astro';
40
+ import { getUser } from '@htlkg/core/auth';
41
+
42
+ export async function GET(context: APIContext) {
43
+ const user = await getUser(context);
44
+
45
+ if (!user) {
46
+ return new Response('Unauthorized', { status: 401 });
47
+ }
48
+
49
+ return new Response(JSON.stringify(user));
50
+ }
51
+ ```
52
+
53
+ In Astro pages:
54
+
55
+ ```astro
56
+ ---
57
+ import { getUser } from '@htlkg/core/auth';
58
+
59
+ const user = await getUser(Astro);
60
+
61
+ if (!user) {
62
+ return Astro.redirect('/login');
63
+ }
64
+ ---
65
+
66
+ <h1>Welcome, {user.username}</h1>
67
+ ```
68
+
69
+ ## Client-Side Authentication
70
+
71
+ ### getClientUser
72
+
73
+ Retrieves the authenticated user on the client side.
74
+
75
+ ```typescript
76
+ import { getClientUser } from '@htlkg/core/auth';
77
+
78
+ const user = await getClientUser();
79
+
80
+ if (user) {
81
+ console.log(`Logged in as ${user.email}`);
82
+ }
83
+ ```
84
+
85
+ ## Access Control Helpers
86
+
87
+ ### hasAccessToBrand
88
+
89
+ Check if user has access to a specific brand.
90
+
91
+ ```typescript
92
+ import { getUser, hasAccessToBrand } from '@htlkg/core/auth';
93
+
94
+ const user = await getUser(context);
95
+ const brandId = 123;
96
+
97
+ if (!hasAccessToBrand(user, brandId)) {
98
+ return new Response('Forbidden', { status: 403 });
99
+ }
100
+ ```
101
+
102
+ ### hasAccessToAccount
103
+
104
+ Check if user has access to a specific account.
105
+
106
+ ```typescript
107
+ import { getUser, hasAccessToAccount } from '@htlkg/core/auth';
108
+
109
+ const user = await getUser(context);
110
+
111
+ if (hasAccessToAccount(user, accountId)) {
112
+ // User can access this account
113
+ }
114
+ ```
115
+
116
+ ### isAdminUser / isSuperAdminUser
117
+
118
+ Check admin status.
119
+
120
+ ```typescript
121
+ import { getUser, isAdminUser, isSuperAdminUser } from '@htlkg/core/auth';
122
+
123
+ const user = await getUser(context);
124
+
125
+ if (isAdminUser(user)) {
126
+ // User is admin or super admin
127
+ }
128
+
129
+ if (isSuperAdminUser(user)) {
130
+ // User is super admin only
131
+ }
132
+ ```
133
+
134
+ ## Cognito Groups
135
+
136
+ The module recognizes these Cognito groups:
137
+
138
+ | Group | Admin | Super Admin |
139
+ |-------|-------|-------------|
140
+ | `admin` | ✓ | ✗ |
141
+ | `ADMINS` | ✓ | ✗ |
142
+ | `SUPER_ADMINS` | ✓ | ✓ |
143
+
144
+ ## Custom Attributes
145
+
146
+ User attributes are parsed from Cognito tokens:
147
+
148
+ - `custom:brand_ids` - Comma-separated brand IDs
149
+ - `custom:account_ids` - Comma-separated account IDs
150
+ - `email` - From ID token
151
+ - `cognito:groups` - User roles/groups
152
+
153
+ ## Error Handling
154
+
155
+ Authentication errors are logged without exposing sensitive data:
156
+
157
+ ```typescript
158
+ const user = await getUser(context);
159
+ // Returns null on any auth error
160
+ // Errors logged with sanitized messages (tokens masked)
161
+ ```
162
+
163
+ ## Middleware Placeholders
164
+
165
+ These functions are placeholders that should be imported from `@htlkg/astro/middleware`:
166
+
167
+ ```typescript
168
+ // These throw errors if called directly from @htlkg/core
169
+ requireAuth(context, loginUrl);
170
+ requireAdminAccess(context, loginUrl);
171
+ requireBrandAccess(context, brandId, loginUrl);
172
+ ```
173
+
174
+ Import from the correct package:
175
+
176
+ ```typescript
177
+ import { requireAuth } from '@htlkg/astro/middleware';
178
+ ```
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ hasAccessToBrand,
4
+ hasAccessToAccount,
5
+ isAdminUser,
6
+ isSuperAdminUser,
7
+ type User,
8
+ } from "./index";
9
+
10
+ describe("Auth Module", () => {
11
+ describe("hasAccessToBrand", () => {
12
+ it("should return false for null user", () => {
13
+ expect(hasAccessToBrand(null, 1)).toBe(false);
14
+ });
15
+
16
+ it("should return true for admin user", () => {
17
+ const user: User = {
18
+ username: "admin",
19
+ email: "admin@test.com",
20
+ brandIds: [],
21
+ accountIds: [],
22
+ isAdmin: true,
23
+ isSuperAdmin: false,
24
+ roles: ["ADMINS"],
25
+ };
26
+ expect(hasAccessToBrand(user, 1)).toBe(true);
27
+ });
28
+
29
+ it("should return true if user has brand access", () => {
30
+ const user: User = {
31
+ username: "user",
32
+ email: "user@test.com",
33
+ brandIds: [1, 2, 3],
34
+ accountIds: [],
35
+ isAdmin: false,
36
+ isSuperAdmin: false,
37
+ roles: [],
38
+ };
39
+ expect(hasAccessToBrand(user, 2)).toBe(true);
40
+ });
41
+
42
+ it("should return false if user does not have brand access", () => {
43
+ const user: User = {
44
+ username: "user",
45
+ email: "user@test.com",
46
+ brandIds: [1, 2, 3],
47
+ accountIds: [],
48
+ isAdmin: false,
49
+ isSuperAdmin: false,
50
+ roles: [],
51
+ };
52
+ expect(hasAccessToBrand(user, 5)).toBe(false);
53
+ });
54
+ });
55
+
56
+ describe("hasAccessToAccount", () => {
57
+ it("should return false for null user", () => {
58
+ expect(hasAccessToAccount(null, 1)).toBe(false);
59
+ });
60
+
61
+ it("should return true for admin user", () => {
62
+ const user: User = {
63
+ username: "admin",
64
+ email: "admin@test.com",
65
+ brandIds: [],
66
+ accountIds: [],
67
+ isAdmin: true,
68
+ isSuperAdmin: false,
69
+ roles: ["ADMINS"],
70
+ };
71
+ expect(hasAccessToAccount(user, 1)).toBe(true);
72
+ });
73
+
74
+ it("should return true if user has account access", () => {
75
+ const user: User = {
76
+ username: "user",
77
+ email: "user@test.com",
78
+ brandIds: [],
79
+ accountIds: [10, 20, 30],
80
+ isAdmin: false,
81
+ isSuperAdmin: false,
82
+ roles: [],
83
+ };
84
+ expect(hasAccessToAccount(user, 20)).toBe(true);
85
+ });
86
+
87
+ it("should return false if user does not have account access", () => {
88
+ const user: User = {
89
+ username: "user",
90
+ email: "user@test.com",
91
+ brandIds: [],
92
+ accountIds: [10, 20, 30],
93
+ isAdmin: false,
94
+ isSuperAdmin: false,
95
+ roles: [],
96
+ };
97
+ expect(hasAccessToAccount(user, 50)).toBe(false);
98
+ });
99
+ });
100
+
101
+ describe("isAdminUser", () => {
102
+ it("should return false for null user", () => {
103
+ expect(isAdminUser(null)).toBe(false);
104
+ });
105
+
106
+ it("should return true for admin user", () => {
107
+ const user: User = {
108
+ username: "admin",
109
+ email: "admin@test.com",
110
+ brandIds: [],
111
+ accountIds: [],
112
+ isAdmin: true,
113
+ isSuperAdmin: false,
114
+ roles: ["ADMINS"],
115
+ };
116
+ expect(isAdminUser(user)).toBe(true);
117
+ });
118
+
119
+ it("should return false for non-admin user", () => {
120
+ const user: User = {
121
+ username: "user",
122
+ email: "user@test.com",
123
+ brandIds: [],
124
+ accountIds: [],
125
+ isAdmin: false,
126
+ isSuperAdmin: false,
127
+ roles: [],
128
+ };
129
+ expect(isAdminUser(user)).toBe(false);
130
+ });
131
+ });
132
+
133
+ describe("isSuperAdminUser", () => {
134
+ it("should return false for null user", () => {
135
+ expect(isSuperAdminUser(null)).toBe(false);
136
+ });
137
+
138
+ it("should return true for super admin user", () => {
139
+ const user: User = {
140
+ username: "superadmin",
141
+ email: "superadmin@test.com",
142
+ brandIds: [],
143
+ accountIds: [],
144
+ isAdmin: true,
145
+ isSuperAdmin: true,
146
+ roles: ["SUPER_ADMINS"],
147
+ };
148
+ expect(isSuperAdminUser(user)).toBe(true);
149
+ });
150
+
151
+ it("should return false for regular admin user", () => {
152
+ const user: User = {
153
+ username: "admin",
154
+ email: "admin@test.com",
155
+ brandIds: [],
156
+ accountIds: [],
157
+ isAdmin: true,
158
+ isSuperAdmin: false,
159
+ roles: ["ADMINS"],
160
+ };
161
+ expect(isSuperAdminUser(user)).toBe(false);
162
+ });
163
+
164
+ it("should return false for non-admin user", () => {
165
+ const user: User = {
166
+ username: "user",
167
+ email: "user@test.com",
168
+ brandIds: [],
169
+ accountIds: [],
170
+ isAdmin: false,
171
+ isSuperAdmin: false,
172
+ roles: [],
173
+ };
174
+ expect(isSuperAdminUser(user)).toBe(false);
175
+ });
176
+ });
177
+
178
+ // Note: getClientUser tests are skipped because they require mocking AWS Amplify
179
+ // which is complex in a unit test environment. These should be tested in integration tests.
180
+ });
@@ -0,0 +1,294 @@
1
+ /**
2
+ * @htlkg/core/auth
3
+ * Core authentication functions and utilities
4
+ */
5
+
6
+ import type { APIContext } from "astro";
7
+ import { Amplify } from "aws-amplify";
8
+ import { fetchAuthSession } from "aws-amplify/auth";
9
+ import {
10
+ fetchAuthSession as fetchServerAuthSession,
11
+ getCurrentUser as getServerCurrentUser,
12
+ } from "aws-amplify/auth/server";
13
+ import { createRunWithAmplifyServerContext, globalSettings } from "../amplify-astro-adapter";
14
+
15
+ // Re-export types
16
+ export type { APIContext } from "astro";
17
+
18
+ /**
19
+ * User interface representing an authenticated user
20
+ */
21
+ export interface User {
22
+ username: string;
23
+ email: string;
24
+ brandIds: number[];
25
+ accountIds: number[];
26
+ isAdmin: boolean;
27
+ isSuperAdmin: boolean;
28
+ roles: string[];
29
+ }
30
+
31
+ /**
32
+ * Parse comma-separated IDs from Cognito custom attributes
33
+ */
34
+ function parseIds(ids: string | undefined): number[] {
35
+ if (!ids) return [];
36
+ return ids
37
+ .split(",")
38
+ .map((id) => Number.parseInt(id.trim(), 10))
39
+ .filter((id) => !Number.isNaN(id));
40
+ }
41
+
42
+ // Track if Amplify has been configured
43
+ let amplifyConfigured = false;
44
+
45
+ /**
46
+ * Configure Amplify on first use (server-side)
47
+ * This must be called before any auth operations
48
+ */
49
+ function ensureAmplifyConfigured(): void {
50
+ if (amplifyConfigured) return;
51
+
52
+ try {
53
+ // Amplify should already be configured by the middleware
54
+ // This is just a safety check
55
+ const config = Amplify.getConfig();
56
+ if (!config.Auth) {
57
+ throw new Error("Amplify Auth not configured");
58
+ }
59
+ amplifyConfigured = true;
60
+ } catch (error) {
61
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
62
+ console.error(`[Auth] Amplify not configured: ${errorMsg}`);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get current authenticated user (server-side)
69
+ * Reads Amplify auth cookies from the request and validates the session
70
+ */
71
+ export async function getUser(context: APIContext): Promise<User | null> {
72
+ try {
73
+ ensureAmplifyConfigured();
74
+
75
+ // Get the current Amplify configuration
76
+ const amplifyConfig = Amplify.getConfig();
77
+
78
+ // Create the server context runner - pass globalSettings explicitly
79
+ // to ensure the same instance is used across all modules
80
+ const runWithAmplifyServerContext = createRunWithAmplifyServerContext({
81
+ config: amplifyConfig,
82
+ globalSettings,
83
+ });
84
+
85
+ // Create Astro server context
86
+ const astroServerContext = {
87
+ cookies: context.cookies,
88
+ request: context.request,
89
+ };
90
+
91
+ // Run in Amplify server context
92
+ const user = await runWithAmplifyServerContext({
93
+ astroServerContext,
94
+ operation: async (contextSpec) => {
95
+ try {
96
+ const currentUser = await getServerCurrentUser(contextSpec as any);
97
+ const session = await fetchServerAuthSession(contextSpec as any);
98
+
99
+ if (!session.tokens?.accessToken) {
100
+ return null;
101
+ }
102
+
103
+ const accessPayload = session.tokens.accessToken.payload;
104
+ const idPayload = session.tokens.idToken?.payload;
105
+
106
+ // Parse user attributes - email is typically in ID token, not access token
107
+ const email =
108
+ (idPayload?.email as string) ||
109
+ (accessPayload.email as string) ||
110
+ "";
111
+ const brandIds = parseIds(
112
+ accessPayload["custom:brand_ids"] as string,
113
+ );
114
+ const accountIds = parseIds(
115
+ accessPayload["custom:account_ids"] as string,
116
+ );
117
+
118
+ // Check admin status
119
+ const groups = (accessPayload["cognito:groups"] as string[]) || [];
120
+ const isAdmin =
121
+ groups.includes("admin") ||
122
+ groups.includes("ADMINS") ||
123
+ groups.includes("SUPER_ADMINS");
124
+ const isSuperAdmin = groups.includes("SUPER_ADMINS");
125
+
126
+ return {
127
+ username: currentUser.username,
128
+ email,
129
+ brandIds,
130
+ accountIds,
131
+ isAdmin,
132
+ isSuperAdmin,
133
+ roles: groups,
134
+ };
135
+ } catch (error) {
136
+ // UserUnAuthenticatedException is expected when user is not logged in
137
+ // Only log other errors without sensitive data
138
+ if (
139
+ error instanceof Error &&
140
+ error.name !== "UserUnAuthenticatedException"
141
+ ) {
142
+ // Log error name and message without stack trace or sensitive data
143
+ console.error(
144
+ "[Auth] Error in server context:",
145
+ error.name,
146
+ "-",
147
+ error.message.replace(/token[=:]\s*[^\s,}]+/gi, "token=***"),
148
+ );
149
+ }
150
+ return null;
151
+ }
152
+ },
153
+ });
154
+
155
+ return user;
156
+ } catch (error) {
157
+ // Log error without exposing sensitive information
158
+ if (error instanceof Error) {
159
+ console.error(
160
+ "[Auth] Server-side auth error:",
161
+ error.name,
162
+ "-",
163
+ error.message.replace(/token[=:]\s*[^\s,}]+/gi, "token=***"),
164
+ );
165
+ } else {
166
+ console.error("[Auth] Server-side auth error: Unknown error");
167
+ }
168
+ return null;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get current authenticated user (client-side)
174
+ */
175
+ export async function getClientUser(): Promise<User | null> {
176
+ try {
177
+ const session = await fetchAuthSession();
178
+
179
+ if (!session.tokens?.accessToken) {
180
+ return null;
181
+ }
182
+
183
+ const accessPayload = session.tokens.accessToken.payload;
184
+ const idPayload = session.tokens.idToken?.payload;
185
+
186
+ const email =
187
+ (idPayload?.email as string) || (accessPayload.email as string) || "";
188
+ const brandIds = parseIds(accessPayload["custom:brand_ids"] as string);
189
+ const accountIds = parseIds(accessPayload["custom:account_ids"] as string);
190
+
191
+ const groups = (accessPayload["cognito:groups"] as string[]) || [];
192
+ const isAdmin =
193
+ groups.includes("admin") ||
194
+ groups.includes("ADMINS") ||
195
+ groups.includes("SUPER_ADMINS");
196
+ const isSuperAdmin = groups.includes("SUPER_ADMINS");
197
+
198
+ return {
199
+ username: (accessPayload.username as string) || "",
200
+ email,
201
+ brandIds,
202
+ accountIds,
203
+ isAdmin,
204
+ isSuperAdmin,
205
+ roles: groups,
206
+ };
207
+ } catch (error) {
208
+ if (error instanceof Error) {
209
+ console.error(
210
+ "[Auth] Client auth error:",
211
+ error.name,
212
+ "-",
213
+ error.message.replace(/token[=:]\s*[^\s,}]+/gi, "token=***"),
214
+ );
215
+ }
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Check if user has access to a specific brand
222
+ */
223
+ export function hasAccessToBrand(
224
+ user: User | null,
225
+ brandId: number,
226
+ ): boolean {
227
+ if (!user) return false;
228
+ if (user.isAdmin) return true;
229
+ return user.brandIds.includes(brandId);
230
+ }
231
+
232
+ /**
233
+ * Check if user has access to a specific account
234
+ */
235
+ export function hasAccessToAccount(
236
+ user: User | null,
237
+ accountId: number,
238
+ ): boolean {
239
+ if (!user) return false;
240
+ if (user.isAdmin) return true;
241
+ return user.accountIds.includes(accountId);
242
+ }
243
+
244
+ /**
245
+ * Check if user is an admin
246
+ */
247
+ export function isAdminUser(user: User | null): boolean {
248
+ return user?.isAdmin || false;
249
+ }
250
+
251
+ /**
252
+ * Check if user is a super admin
253
+ */
254
+ export function isSuperAdminUser(user: User | null): boolean {
255
+ return user?.isSuperAdmin || false;
256
+ }
257
+
258
+ /**
259
+ * Placeholder for requireAuth - will be implemented in @htlkg/integrations
260
+ * This is here for type exports and documentation
261
+ */
262
+ export async function requireAuth(
263
+ context: APIContext,
264
+ loginUrl?: string,
265
+ ): Promise<{ success: boolean; user?: User; redirectTo?: string }> {
266
+ throw new Error(
267
+ "requireAuth should be imported from @htlkg/astro/middleware",
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Placeholder for requireAdminAccess - will be implemented in @htlkg/integrations
273
+ */
274
+ export async function requireAdminAccess(
275
+ context: APIContext,
276
+ loginUrl?: string,
277
+ ): Promise<{ success: boolean; user?: User; redirectTo?: string }> {
278
+ throw new Error(
279
+ "requireAdminAccess should be imported from @htlkg/astro/middleware",
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Placeholder for requireBrandAccess - will be implemented in @htlkg/integrations
285
+ */
286
+ export async function requireBrandAccess(
287
+ context: APIContext,
288
+ brandId: number,
289
+ loginUrl?: string,
290
+ ): Promise<{ success: boolean; user?: User; redirectTo?: string }> {
291
+ throw new Error(
292
+ "requireBrandAccess should be imported from @htlkg/astro/middleware",
293
+ );
294
+ }