@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.
- package/README.md +51 -0
- package/dist/index.d.ts +219 -0
- package/dist/index.js +121 -0
- package/dist/index.js.map +1 -1
- package/package.json +30 -8
- package/src/amplify-astro-adapter/amplify-astro-adapter.md +167 -0
- package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.test.ts +296 -0
- package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.ts +97 -0
- package/src/amplify-astro-adapter/createRunWithAmplifyServerContext.ts +84 -0
- package/src/amplify-astro-adapter/errors.test.ts +115 -0
- package/src/amplify-astro-adapter/errors.ts +105 -0
- package/src/amplify-astro-adapter/globalSettings.test.ts +78 -0
- package/src/amplify-astro-adapter/globalSettings.ts +16 -0
- package/src/amplify-astro-adapter/index.ts +14 -0
- package/src/amplify-astro-adapter/types.ts +55 -0
- package/src/auth/auth.md +178 -0
- package/src/auth/index.test.ts +180 -0
- package/src/auth/index.ts +294 -0
- package/src/constants/constants.md +132 -0
- package/src/constants/index.test.ts +116 -0
- package/src/constants/index.ts +98 -0
- package/src/core-exports.property.test.ts +186 -0
- package/src/errors/errors.md +177 -0
- package/src/errors/index.test.ts +153 -0
- package/src/errors/index.ts +134 -0
- package/src/index.ts +65 -0
- package/src/routes/index.ts +225 -0
- package/src/routes/routes.md +189 -0
- package/src/types/index.ts +94 -0
- package/src/types/types.md +144 -0
- package/src/utils/index.test.ts +257 -0
- package/src/utils/index.ts +112 -0
- package/src/utils/logger.ts +88 -0
- package/src/utils/utils.md +199 -0
- package/src/workspace.property.test.ts +235 -0
package/package.json
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@htlkg/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".":
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"./
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts"
|
|
9
|
+
},
|
|
10
|
+
"./auth": {
|
|
11
|
+
"import": "./dist/auth/index.js",
|
|
12
|
+
"types": "./dist/auth/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./types": {
|
|
15
|
+
"import": "./dist/types/index.js",
|
|
16
|
+
"types": "./dist/types/index.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./utils": {
|
|
19
|
+
"import": "./dist/utils/index.js",
|
|
20
|
+
"types": "./dist/utils/index.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./constants": {
|
|
23
|
+
"import": "./dist/constants/index.js",
|
|
24
|
+
"types": "./dist/constants/index.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"./errors": {
|
|
27
|
+
"import": "./dist/errors/index.js",
|
|
28
|
+
"types": "./dist/errors/index.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./amplify-astro-adapter": {
|
|
31
|
+
"import": "./dist/amplify-astro-adapter/index.js",
|
|
32
|
+
"types": "./dist/amplify-astro-adapter/index.d.ts"
|
|
33
|
+
}
|
|
13
34
|
},
|
|
14
35
|
"files": [
|
|
36
|
+
"src",
|
|
15
37
|
"dist"
|
|
16
38
|
],
|
|
17
39
|
"dependencies": {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Amplify Astro Adapter
|
|
2
|
+
|
|
3
|
+
Server-side authentication support for Astro applications using AWS Amplify. Handles cookie-based authentication context for SSR.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
createRunWithAmplifyServerContext,
|
|
10
|
+
createCookieStorageAdapterFromAstroContext,
|
|
11
|
+
globalSettings
|
|
12
|
+
} from '@htlkg/core/amplify-astro-adapter';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Core Functions
|
|
16
|
+
|
|
17
|
+
### createRunWithAmplifyServerContext
|
|
18
|
+
|
|
19
|
+
Creates a function that runs operations within an authenticated Amplify server context.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Amplify } from 'aws-amplify';
|
|
23
|
+
import { createRunWithAmplifyServerContext, globalSettings } from '@htlkg/core/amplify-astro-adapter';
|
|
24
|
+
|
|
25
|
+
const amplifyConfig = Amplify.getConfig();
|
|
26
|
+
|
|
27
|
+
const runWithAmplifyServerContext = createRunWithAmplifyServerContext({
|
|
28
|
+
config: amplifyConfig,
|
|
29
|
+
globalSettings, // Pass explicitly to avoid module singleton issues
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Use in Astro page or API route
|
|
33
|
+
const result = await runWithAmplifyServerContext({
|
|
34
|
+
astroServerContext: {
|
|
35
|
+
cookies: Astro.cookies,
|
|
36
|
+
request: Astro.request,
|
|
37
|
+
},
|
|
38
|
+
operation: async (contextSpec) => {
|
|
39
|
+
// Perform authenticated operations
|
|
40
|
+
const user = await getCurrentUser(contextSpec);
|
|
41
|
+
return user;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### createCookieStorageAdapterFromAstroContext
|
|
47
|
+
|
|
48
|
+
Creates a cookie storage adapter compatible with Amplify's auth system.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createCookieStorageAdapterFromAstroContext } from '@htlkg/core/amplify-astro-adapter';
|
|
52
|
+
|
|
53
|
+
const cookieAdapter = await createCookieStorageAdapterFromAstroContext({
|
|
54
|
+
cookies: Astro.cookies,
|
|
55
|
+
request: Astro.request,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Adapter methods
|
|
59
|
+
cookieAdapter.get('cookie-name');
|
|
60
|
+
cookieAdapter.getAll();
|
|
61
|
+
cookieAdapter.set('name', 'value', options);
|
|
62
|
+
cookieAdapter.delete('name');
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### globalSettings
|
|
66
|
+
|
|
67
|
+
Runtime configuration for the adapter.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { globalSettings } from '@htlkg/core/amplify-astro-adapter';
|
|
71
|
+
|
|
72
|
+
// Enable server-side auth (enabled by default)
|
|
73
|
+
globalSettings.enableServerSideAuth();
|
|
74
|
+
|
|
75
|
+
// Check if SSL origin (affects secure cookie flag)
|
|
76
|
+
globalSettings.setIsSSLOrigin(true);
|
|
77
|
+
|
|
78
|
+
// Set custom cookie options
|
|
79
|
+
globalSettings.setRuntimeOptions({
|
|
80
|
+
cookies: {
|
|
81
|
+
domain: '.example.com',
|
|
82
|
+
sameSite: 'lax',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Error Handling
|
|
88
|
+
|
|
89
|
+
The adapter provides specific error classes for different failure scenarios:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import {
|
|
93
|
+
AmplifyAstroAdapterError,
|
|
94
|
+
ErrorCodes,
|
|
95
|
+
createAuthFailedError,
|
|
96
|
+
createCookieError,
|
|
97
|
+
createConfigMissingError,
|
|
98
|
+
} from '@htlkg/core/amplify-astro-adapter';
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// ... adapter operations
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof AmplifyAstroAdapterError) {
|
|
104
|
+
console.error(`[${error.code}] ${error.message}`);
|
|
105
|
+
if (error.recoverySuggestion) {
|
|
106
|
+
console.log('Suggestion:', error.recoverySuggestion);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Error Codes
|
|
113
|
+
|
|
114
|
+
| Code | Description |
|
|
115
|
+
|------|-------------|
|
|
116
|
+
| `CONFIG_MISSING` | Amplify configuration not found |
|
|
117
|
+
| `AUTH_FAILED` | Authentication failed |
|
|
118
|
+
| `COOKIE_ERROR` | Cookie read/write error |
|
|
119
|
+
| `GRAPHQL_ERROR` | GraphQL operation failed |
|
|
120
|
+
| `CONTEXT_CREATION_FAILED` | Server context creation failed |
|
|
121
|
+
| `TOKEN_REFRESH_FAILED` | Token refresh failed |
|
|
122
|
+
|
|
123
|
+
## Types
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import type { AstroServer, AstroAmplifyConfig } from '@htlkg/core/amplify-astro-adapter';
|
|
127
|
+
|
|
128
|
+
// Server context types
|
|
129
|
+
type ServerContext = AstroServer.AstroComponentContext | AstroServer.APIEndpointContext | null;
|
|
130
|
+
|
|
131
|
+
// Runtime options
|
|
132
|
+
interface RuntimeOptions {
|
|
133
|
+
cookies?: RuntimeCookieOptions;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Run operation signature
|
|
137
|
+
type RunOperationWithContext = <T>(input: {
|
|
138
|
+
astroServerContext: ServerContext;
|
|
139
|
+
operation: (contextSpec: unknown) => Promise<T>;
|
|
140
|
+
}) => Promise<T>;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Cookie Handling
|
|
144
|
+
|
|
145
|
+
The adapter handles Cognito authentication cookies with:
|
|
146
|
+
|
|
147
|
+
- Automatic encoding compatible with js-cookie
|
|
148
|
+
- Secure defaults (`httpOnly`, `sameSite: 'lax'`, `secure` based on SSL)
|
|
149
|
+
- Support for both Astro component and API endpoint contexts
|
|
150
|
+
|
|
151
|
+
## Debug Logging
|
|
152
|
+
|
|
153
|
+
Enable debug logs by setting environment variables:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
DEBUG=true
|
|
157
|
+
# or
|
|
158
|
+
HTLKG_DEBUG=true
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { createLogger } from '@htlkg/core/amplify-astro-adapter';
|
|
163
|
+
|
|
164
|
+
const log = createLogger('my-module');
|
|
165
|
+
log.debug('Debug message'); // Only shown when DEBUG=true
|
|
166
|
+
log.info('Info message'); // Always shown
|
|
167
|
+
```
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { createCookieStorageAdapterFromAstroContext } from "./createCookieStorageAdapterFromAstroContext";
|
|
3
|
+
import type { AstroServer } from "./types";
|
|
4
|
+
|
|
5
|
+
describe("createCookieStorageAdapterFromAstroContext", () => {
|
|
6
|
+
describe("with null context", () => {
|
|
7
|
+
it("should return a no-op adapter", async () => {
|
|
8
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(null as any);
|
|
9
|
+
|
|
10
|
+
expect(adapter.get("test")).toBeUndefined();
|
|
11
|
+
expect(adapter.getAll()).toEqual([]);
|
|
12
|
+
|
|
13
|
+
// These should not throw
|
|
14
|
+
adapter.set("test", "value");
|
|
15
|
+
adapter.delete("test");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("with valid context", () => {
|
|
20
|
+
it("should get cookie from Astro cookies", async () => {
|
|
21
|
+
const mockCookies = {
|
|
22
|
+
get: vi.fn((name: string) => ({ name, value: "cookie-value" })),
|
|
23
|
+
set: vi.fn(),
|
|
24
|
+
delete: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const mockRequest = new Request("https://example.com", {
|
|
28
|
+
headers: { cookie: "" },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const context: AstroServer.ServerContext = {
|
|
32
|
+
cookies: mockCookies as any,
|
|
33
|
+
request: mockRequest,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
37
|
+
const result = adapter.get("test");
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual({ name: "test", value: "cookie-value" });
|
|
40
|
+
expect(mockCookies.get).toHaveBeenCalledWith("test");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should get cookie from request headers if not in Astro cookies", async () => {
|
|
44
|
+
// When cookies.get returns an object without value property, it falls back to headers
|
|
45
|
+
const mockCookies = {
|
|
46
|
+
get: vi.fn(() => ({ value: undefined })),
|
|
47
|
+
set: vi.fn(),
|
|
48
|
+
delete: vi.fn(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const mockRequest = new Request("https://example.com", {
|
|
52
|
+
headers: { cookie: "test=header-value; other=value2" },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const context: AstroServer.ServerContext = {
|
|
56
|
+
cookies: mockCookies as any,
|
|
57
|
+
request: mockRequest,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
61
|
+
const result = adapter.get("test");
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({ name: "test", value: "header-value" });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should handle URL-encoded cookie values", async () => {
|
|
67
|
+
const mockCookies = {
|
|
68
|
+
get: vi.fn(() => ({ value: undefined })),
|
|
69
|
+
set: vi.fn(),
|
|
70
|
+
delete: vi.fn(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const mockRequest = new Request("https://example.com", {
|
|
74
|
+
headers: { cookie: "test=hello%20world" },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const context: AstroServer.ServerContext = {
|
|
78
|
+
cookies: mockCookies as any,
|
|
79
|
+
request: mockRequest,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
83
|
+
const result = adapter.get("test");
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual({ name: "test", value: "hello world" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should get all cookies from getAll method", async () => {
|
|
89
|
+
const mockCookies = {
|
|
90
|
+
get: vi.fn(),
|
|
91
|
+
getAll: vi.fn(() => [
|
|
92
|
+
{ name: "cookie1", value: "value1" },
|
|
93
|
+
{ name: "cookie2", value: "value2" },
|
|
94
|
+
]),
|
|
95
|
+
set: vi.fn(),
|
|
96
|
+
delete: vi.fn(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mockRequest = new Request("https://example.com", {
|
|
100
|
+
headers: { cookie: "" },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const context: AstroServer.ServerContext = {
|
|
104
|
+
cookies: mockCookies as any,
|
|
105
|
+
request: mockRequest,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
109
|
+
const result = adapter.getAll();
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual([
|
|
112
|
+
{ name: "cookie1", value: "value1" },
|
|
113
|
+
{ name: "cookie2", value: "value2" },
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should get all cookies from headers when getAll is not available", async () => {
|
|
118
|
+
const mockCookies = {
|
|
119
|
+
get: vi.fn(),
|
|
120
|
+
set: vi.fn(),
|
|
121
|
+
delete: vi.fn(),
|
|
122
|
+
// No getAll method
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const mockRequest = new Request("https://example.com", {
|
|
126
|
+
headers: { cookie: "cookie1=value1; cookie2=value2" },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const context: AstroServer.ServerContext = {
|
|
130
|
+
cookies: mockCookies as any,
|
|
131
|
+
request: mockRequest,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
135
|
+
const result = adapter.getAll();
|
|
136
|
+
|
|
137
|
+
expect(result).toEqual([
|
|
138
|
+
{ name: "cookie1", value: "value1" },
|
|
139
|
+
{ name: "cookie2", value: "value2" },
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should set cookie with options", async () => {
|
|
144
|
+
const mockCookies = {
|
|
145
|
+
get: vi.fn(),
|
|
146
|
+
set: vi.fn(),
|
|
147
|
+
delete: vi.fn(),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const mockRequest = new Request("https://example.com", {
|
|
151
|
+
headers: { cookie: "" },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const context: AstroServer.ServerContext = {
|
|
155
|
+
cookies: mockCookies as any,
|
|
156
|
+
request: mockRequest,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
160
|
+
adapter.set("test", "value", {
|
|
161
|
+
httpOnly: true,
|
|
162
|
+
secure: true,
|
|
163
|
+
sameSite: "lax",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(mockCookies.set).toHaveBeenCalledWith("test", "value", {
|
|
167
|
+
httpOnly: true,
|
|
168
|
+
secure: true,
|
|
169
|
+
sameSite: "lax",
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should map sameSite boolean values", async () => {
|
|
174
|
+
const mockCookies = {
|
|
175
|
+
get: vi.fn(),
|
|
176
|
+
set: vi.fn(),
|
|
177
|
+
delete: vi.fn(),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const mockRequest = new Request("https://example.com", {
|
|
181
|
+
headers: { cookie: "" },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const context: AstroServer.ServerContext = {
|
|
185
|
+
cookies: mockCookies as any,
|
|
186
|
+
request: mockRequest,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
190
|
+
|
|
191
|
+
// sameSite: true -> 'strict'
|
|
192
|
+
adapter.set("test1", "value1", { sameSite: true as any });
|
|
193
|
+
expect(mockCookies.set).toHaveBeenCalledWith("test1", "value1", {
|
|
194
|
+
sameSite: "strict",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// sameSite: false -> 'lax'
|
|
198
|
+
adapter.set("test2", "value2", { sameSite: false as any });
|
|
199
|
+
expect(mockCookies.set).toHaveBeenCalledWith("test2", "value2", {
|
|
200
|
+
sameSite: "lax",
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should delete cookie", async () => {
|
|
205
|
+
const mockCookies = {
|
|
206
|
+
get: vi.fn(),
|
|
207
|
+
set: vi.fn(),
|
|
208
|
+
delete: vi.fn(),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const mockRequest = new Request("https://example.com", {
|
|
212
|
+
headers: { cookie: "" },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const context: AstroServer.ServerContext = {
|
|
216
|
+
cookies: mockCookies as any,
|
|
217
|
+
request: mockRequest,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
221
|
+
adapter.delete("test");
|
|
222
|
+
|
|
223
|
+
expect(mockCookies.delete).toHaveBeenCalledWith("test");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should handle errors gracefully when setting cookies", async () => {
|
|
227
|
+
const mockCookies = {
|
|
228
|
+
get: vi.fn(),
|
|
229
|
+
set: vi.fn(() => {
|
|
230
|
+
throw new Error("Cookie error");
|
|
231
|
+
}),
|
|
232
|
+
delete: vi.fn(),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const mockRequest = new Request("https://example.com", {
|
|
236
|
+
headers: { cookie: "" },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const context: AstroServer.ServerContext = {
|
|
240
|
+
cookies: mockCookies as any,
|
|
241
|
+
request: mockRequest,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
245
|
+
|
|
246
|
+
// Should not throw
|
|
247
|
+
expect(() => adapter.set("test", "value")).not.toThrow();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should handle errors gracefully when deleting cookies", async () => {
|
|
251
|
+
const mockCookies = {
|
|
252
|
+
get: vi.fn(),
|
|
253
|
+
set: vi.fn(),
|
|
254
|
+
delete: vi.fn(() => {
|
|
255
|
+
throw new Error("Delete error");
|
|
256
|
+
}),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const mockRequest = new Request("https://example.com", {
|
|
260
|
+
headers: { cookie: "" },
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const context: AstroServer.ServerContext = {
|
|
264
|
+
cookies: mockCookies as any,
|
|
265
|
+
request: mockRequest,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
269
|
+
|
|
270
|
+
// Should not throw
|
|
271
|
+
expect(() => adapter.delete("test")).not.toThrow();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should return undefined when cookie not found anywhere", async () => {
|
|
275
|
+
const mockCookies = {
|
|
276
|
+
get: vi.fn(() => ({ value: undefined })),
|
|
277
|
+
set: vi.fn(),
|
|
278
|
+
delete: vi.fn(),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const mockRequest = new Request("https://example.com", {
|
|
282
|
+
headers: { cookie: "" },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const context: AstroServer.ServerContext = {
|
|
286
|
+
cookies: mockCookies as any,
|
|
287
|
+
request: mockRequest,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const adapter = await createCookieStorageAdapterFromAstroContext(context);
|
|
291
|
+
const result = adapter.get("nonexistent");
|
|
292
|
+
|
|
293
|
+
expect(result).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CookieStorage } from 'aws-amplify/adapter-core';
|
|
2
|
+
import type { AstroServer } from './types';
|
|
3
|
+
import { createLogger } from '../utils/logger';
|
|
4
|
+
|
|
5
|
+
const log = createLogger('cookie-storage-adapter');
|
|
6
|
+
|
|
7
|
+
// Ensures the cookie names are encoded in order to look up the cookie store
|
|
8
|
+
// that is manipulated by js-cookie on the client side.
|
|
9
|
+
// Details of the js-cookie encoding behavior see:
|
|
10
|
+
// https://github.com/js-cookie/js-cookie#encoding
|
|
11
|
+
// The implementation is borrowed from js-cookie without escaping `[()]` as
|
|
12
|
+
// we are not using those chars in the auth keys.
|
|
13
|
+
function ensureEncodedForJSCookie(name: string): string {
|
|
14
|
+
return encodeURIComponent(name).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseCookieHeader(cookieHeader: string | null) {
|
|
18
|
+
const out: Record<string, string> = {};
|
|
19
|
+
if (!cookieHeader) return out;
|
|
20
|
+
for (const part of cookieHeader.split(';')) {
|
|
21
|
+
const [rawName, ...rest] = part.trim().split('=');
|
|
22
|
+
const name = decodeURIComponent(rawName || '');
|
|
23
|
+
const value = decodeURIComponent(rest.join('=') || '');
|
|
24
|
+
if (name) out[name] = value;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mapSameSite(
|
|
30
|
+
s: CookieStorage.SetCookieOptions['sameSite']
|
|
31
|
+
): 'lax' | 'strict' | 'none' | undefined {
|
|
32
|
+
if (s === true) return 'strict';
|
|
33
|
+
if (s === false) return 'lax';
|
|
34
|
+
if (s === 'lax' || s === 'strict' || s === 'none') return s;
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function createCookieStorageAdapterFromAstroContext(
|
|
39
|
+
astroServerContext: AstroServer.ServerContext
|
|
40
|
+
): Promise<CookieStorage.Adapter> {
|
|
41
|
+
if (astroServerContext === null) {
|
|
42
|
+
log.debug('Context is null, returning no-op adapter');
|
|
43
|
+
return {
|
|
44
|
+
get: () => undefined,
|
|
45
|
+
getAll: () => [],
|
|
46
|
+
set: () => {},
|
|
47
|
+
delete: () => {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { cookies, request } = astroServerContext;
|
|
52
|
+
const cookieHeader = request?.headers?.get('cookie');
|
|
53
|
+
const headerCookies = parseCookieHeader(cookieHeader ?? null);
|
|
54
|
+
|
|
55
|
+
// Debug: Log available cookies (names only for security)
|
|
56
|
+
const cookieNames = Object.keys(headerCookies);
|
|
57
|
+
const cognitoCookies = cookieNames.filter(name => name.includes('CognitoIdentityServiceProvider'));
|
|
58
|
+
log.debug('Available cookies:', cookieNames.length);
|
|
59
|
+
log.debug('Cognito cookies found:', cognitoCookies.length);
|
|
60
|
+
if (cognitoCookies.length > 0) {
|
|
61
|
+
log.debug('Cognito cookie names:', cognitoCookies);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const adapter: CookieStorage.Adapter = {
|
|
65
|
+
get(name) {
|
|
66
|
+
const encodedName = ensureEncodedForJSCookie(name);
|
|
67
|
+
const v = cookies?.get(encodedName)?.value ?? headerCookies[encodedName] ?? headerCookies[name];
|
|
68
|
+
// Debug: Log token lookups (only for auth-related cookies)
|
|
69
|
+
if (name.includes('accessToken') || name.includes('idToken') || name.includes('refreshToken') || name.includes('LastAuthUser')) {
|
|
70
|
+
log.debug(`get('${name}'): ${v ? 'FOUND' : 'NOT FOUND'}`);
|
|
71
|
+
}
|
|
72
|
+
return v ? { name, value: v } : undefined;
|
|
73
|
+
},
|
|
74
|
+
getAll() {
|
|
75
|
+
const fromAPI =
|
|
76
|
+
(typeof (cookies as any)?.getAll === 'function'
|
|
77
|
+
? (cookies as any).getAll().map((c: any) => ({ name: c.name, value: c.value }))
|
|
78
|
+
: Object.entries(headerCookies).map(([name, value]) => ({ name, value })));
|
|
79
|
+
return fromAPI;
|
|
80
|
+
},
|
|
81
|
+
set(name, value, options) {
|
|
82
|
+
try {
|
|
83
|
+
(cookies as any).set(name, value, {
|
|
84
|
+
...(options ?? {}),
|
|
85
|
+
sameSite: mapSameSite(options?.sameSite),
|
|
86
|
+
});
|
|
87
|
+
} catch {}
|
|
88
|
+
},
|
|
89
|
+
delete(name: string) {
|
|
90
|
+
try {
|
|
91
|
+
(cookies as any).delete(name);
|
|
92
|
+
} catch {}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return adapter;
|
|
97
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ResourcesConfig } from 'aws-amplify';
|
|
2
|
+
import { sharedInMemoryStorage } from 'aws-amplify/utils';
|
|
3
|
+
import {
|
|
4
|
+
createAWSCredentialsAndIdentityIdProvider,
|
|
5
|
+
createKeyValueStorageFromCookieStorageAdapter,
|
|
6
|
+
createUserPoolsTokenProvider,
|
|
7
|
+
runWithAmplifyServerContext as coreRunWithContext,
|
|
8
|
+
} from 'aws-amplify/adapter-core';
|
|
9
|
+
|
|
10
|
+
import type { AstroServer } from './types';
|
|
11
|
+
import { globalSettings as defaultGlobalSettings } from './globalSettings';
|
|
12
|
+
import { createCookieStorageAdapterFromAstroContext } from './createCookieStorageAdapterFromAstroContext';
|
|
13
|
+
import { createLogger } from '../utils/logger';
|
|
14
|
+
|
|
15
|
+
const log = createLogger('amplify-server-context');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a function that runs operations within the Amplify server context.
|
|
19
|
+
*
|
|
20
|
+
* IMPORTANT: Pass globalSettings explicitly to avoid module singleton issues
|
|
21
|
+
* when using linked packages or different bundling contexts.
|
|
22
|
+
*/
|
|
23
|
+
export const createRunWithAmplifyServerContext = ({
|
|
24
|
+
config: resourcesConfig,
|
|
25
|
+
globalSettings = defaultGlobalSettings,
|
|
26
|
+
}: {
|
|
27
|
+
config: ResourcesConfig;
|
|
28
|
+
globalSettings?: AstroServer.GlobalSettings;
|
|
29
|
+
}): AstroServer.RunOperationWithContext => {
|
|
30
|
+
const isServerSideAuthEnabled = globalSettings.isServerSideAuthEnabled();
|
|
31
|
+
const isSSLOrigin = globalSettings.isSSLOrigin();
|
|
32
|
+
const setCookieOptions = globalSettings.getRuntimeOptions().cookies ?? {};
|
|
33
|
+
|
|
34
|
+
log.debug('Settings:', {
|
|
35
|
+
isServerSideAuthEnabled,
|
|
36
|
+
isSSLOrigin,
|
|
37
|
+
hasCookieOptions: Object.keys(setCookieOptions).length > 0,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const mergedSetCookieOptions = {
|
|
41
|
+
...(isServerSideAuthEnabled && { httpOnly: true, sameSite: 'lax' as const }),
|
|
42
|
+
...setCookieOptions,
|
|
43
|
+
...(isServerSideAuthEnabled && { secure: isSSLOrigin }),
|
|
44
|
+
path: '/',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const runWithContext: AstroServer.RunOperationWithContext = async ({
|
|
48
|
+
astroServerContext,
|
|
49
|
+
operation,
|
|
50
|
+
}) => {
|
|
51
|
+
if (resourcesConfig.Auth) {
|
|
52
|
+
const cookieAdapter = await createCookieStorageAdapterFromAstroContext(astroServerContext);
|
|
53
|
+
|
|
54
|
+
const keyValueStorage =
|
|
55
|
+
astroServerContext === null
|
|
56
|
+
? sharedInMemoryStorage
|
|
57
|
+
: createKeyValueStorageFromCookieStorageAdapter(
|
|
58
|
+
cookieAdapter,
|
|
59
|
+
undefined,
|
|
60
|
+
mergedSetCookieOptions
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const credentialsProvider = createAWSCredentialsAndIdentityIdProvider(
|
|
64
|
+
resourcesConfig.Auth,
|
|
65
|
+
keyValueStorage
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const tokenProvider = createUserPoolsTokenProvider(
|
|
69
|
+
resourcesConfig.Auth,
|
|
70
|
+
keyValueStorage
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return coreRunWithContext(
|
|
74
|
+
resourcesConfig,
|
|
75
|
+
{ Auth: { credentialsProvider, tokenProvider } },
|
|
76
|
+
operation
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return coreRunWithContext(resourcesConfig, {}, operation);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return runWithContext;
|
|
84
|
+
};
|