@prmichaelsen/remember-mcp 2.2.1 → 2.3.1
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/AGENT.md +98 -5
- package/CHANGELOG.md +45 -0
- package/README.md +43 -3
- package/agent/commands/acp.init.md +376 -0
- package/agent/commands/acp.package-install.md +347 -0
- package/agent/commands/acp.proceed.md +311 -0
- package/agent/commands/acp.report.md +392 -0
- package/agent/commands/acp.status.md +280 -0
- package/agent/commands/acp.sync.md +323 -0
- package/agent/commands/acp.update.md +301 -0
- package/agent/commands/acp.validate.md +385 -0
- package/agent/commands/acp.version-check-for-updates.md +275 -0
- package/agent/commands/acp.version-check.md +190 -0
- package/agent/commands/acp.version-update.md +288 -0
- package/agent/commands/command.template.md +273 -0
- package/agent/design/core-memory-user-profile.md +1253 -0
- package/agent/design/ghost-profiles-pseudonymous-identity.md +194 -0
- package/agent/design/publish-tools-confirmation-flow.md +922 -0
- package/agent/milestones/milestone-10-shared-spaces.md +169 -0
- package/agent/progress.yaml +90 -4
- package/agent/scripts/install.sh +118 -0
- package/agent/scripts/update.sh +22 -10
- package/agent/scripts/version.sh +35 -0
- package/agent/tasks/task-27-implement-llm-provider-interface.md +51 -0
- package/agent/tasks/task-28-implement-llm-provider-factory.md +64 -0
- package/agent/tasks/task-29-update-config-for-llm.md +71 -0
- package/agent/tasks/task-30-implement-bedrock-provider.md +147 -0
- package/agent/tasks/task-31-implement-background-job-service.md +120 -0
- package/agent/tasks/task-32-test-llm-provider-integration.md +152 -0
- package/agent/tasks/task-34-create-confirmation-token-service.md +191 -0
- package/agent/tasks/task-35-create-space-memory-types-schema.md +183 -0
- package/agent/tasks/task-36-implement-remember-publish.md +227 -0
- package/agent/tasks/task-37-implement-remember-confirm.md +225 -0
- package/agent/tasks/task-38-implement-remember-deny.md +161 -0
- package/agent/tasks/task-39-implement-remember-search-space.md +188 -0
- package/agent/tasks/task-40-implement-remember-query-space.md +193 -0
- package/agent/tasks/task-41-configure-firestore-ttl.md +188 -0
- package/agent/tasks/task-42-create-tests-shared-spaces.md +216 -0
- package/agent/tasks/task-43-update-documentation.md +255 -0
- package/agent/tasks/task-44-implement-remember-retract.md +263 -0
- package/agent/tasks/task-45-fix-publish-false-success-bug.md +230 -0
- package/dist/llm/types.d.ts +1 -0
- package/dist/server-factory.js +1000 -1
- package/dist/server.js +1002 -3
- package/dist/services/confirmation-token.service.d.ts +99 -0
- package/dist/services/confirmation-token.service.spec.d.ts +5 -0
- package/dist/tools/confirm.d.ts +20 -0
- package/dist/tools/deny.d.ts +19 -0
- package/dist/tools/publish.d.ts +22 -0
- package/dist/tools/query-space.d.ts +28 -0
- package/dist/tools/search-space.d.ts +29 -0
- package/dist/types/space-memory.d.ts +80 -0
- package/dist/weaviate/space-schema.d.ts +59 -0
- package/dist/weaviate/space-schema.spec.d.ts +5 -0
- package/package.json +1 -1
- package/src/llm/types.ts +0 -0
- package/src/server-factory.ts +33 -0
- package/src/server.ts +33 -0
- package/src/services/confirmation-token.service.spec.ts +254 -0
- package/src/services/confirmation-token.service.ts +265 -0
- package/src/tools/confirm.ts +219 -0
- package/src/tools/create-memory.ts +7 -0
- package/src/tools/deny.ts +70 -0
- package/src/tools/publish.ts +190 -0
- package/src/tools/query-space.ts +197 -0
- package/src/tools/search-space.ts +189 -0
- package/src/types/space-memory.ts +94 -0
- package/src/weaviate/space-schema.spec.ts +131 -0
- package/src/weaviate/space-schema.ts +275 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Confirmation Token Service
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ConfirmationTokenService, type ConfirmationRequest } from '../../src/services/confirmation-token.service';
|
|
6
|
+
import * as firestoreInit from '../../src/firestore/init';
|
|
7
|
+
|
|
8
|
+
// Mock Firestore functions
|
|
9
|
+
jest.mock('../../src/firestore/init', () => ({
|
|
10
|
+
getDocument: jest.fn(),
|
|
11
|
+
addDocument: jest.fn(),
|
|
12
|
+
updateDocument: jest.fn(),
|
|
13
|
+
queryDocuments: jest.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('ConfirmationTokenService', () => {
|
|
17
|
+
let service: ConfirmationTokenService;
|
|
18
|
+
const mockUserId = 'test-user-123';
|
|
19
|
+
const mockToken = '550e8400-e29b-41d4-a716-446655440000';
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
service = new ConfirmationTokenService();
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
jest.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('createRequest', () => {
|
|
31
|
+
it('should create a new confirmation request with token', async () => {
|
|
32
|
+
const mockDocRef = { id: 'request-123', path: 'users/test-user-123/requests/request-123' };
|
|
33
|
+
(firestoreInit.addDocument as jest.Mock).mockResolvedValue(mockDocRef);
|
|
34
|
+
|
|
35
|
+
const payload = { memory_id: 'mem-123', additional_tags: [] };
|
|
36
|
+
const result = await service.createRequest(mockUserId, 'publish_memory', payload, 'the_void');
|
|
37
|
+
|
|
38
|
+
expect(result.requestId).toBe('request-123');
|
|
39
|
+
expect(result.token).toBeDefined();
|
|
40
|
+
expect(typeof result.token).toBe('string');
|
|
41
|
+
expect(result.token.length).toBeGreaterThan(0);
|
|
42
|
+
expect(firestoreInit.addDocument).toHaveBeenCalledWith(
|
|
43
|
+
'users/test-user-123/requests',
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
user_id: mockUserId,
|
|
46
|
+
action: 'publish_memory',
|
|
47
|
+
target_collection: 'the_void',
|
|
48
|
+
payload,
|
|
49
|
+
status: 'pending',
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should set expiry to 5 minutes from now', async () => {
|
|
55
|
+
const mockDocRef = { id: 'request-123', path: 'path' };
|
|
56
|
+
(firestoreInit.addDocument as jest.Mock).mockResolvedValue(mockDocRef);
|
|
57
|
+
|
|
58
|
+
const beforeTime = Date.now();
|
|
59
|
+
await service.createRequest(mockUserId, 'publish_memory', {});
|
|
60
|
+
const afterTime = Date.now();
|
|
61
|
+
|
|
62
|
+
const call = (firestoreInit.addDocument as jest.Mock).mock.calls[0][1];
|
|
63
|
+
const expiresAt = new Date(call.expires_at).getTime();
|
|
64
|
+
const createdAt = new Date(call.created_at).getTime();
|
|
65
|
+
|
|
66
|
+
// Should be 5 minutes (300000ms) after creation
|
|
67
|
+
const expectedExpiry = createdAt + 5 * 60 * 1000;
|
|
68
|
+
expect(expiresAt).toBe(expectedExpiry);
|
|
69
|
+
expect(createdAt).toBeGreaterThanOrEqual(beforeTime);
|
|
70
|
+
expect(createdAt).toBeLessThanOrEqual(afterTime);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('validateToken', () => {
|
|
75
|
+
it('should return request if token is valid and not expired', async () => {
|
|
76
|
+
const mockRequest: ConfirmationRequest = {
|
|
77
|
+
user_id: mockUserId,
|
|
78
|
+
token: mockToken,
|
|
79
|
+
action: 'publish_memory',
|
|
80
|
+
payload: { memory_id: 'mem-123' },
|
|
81
|
+
created_at: new Date().toISOString(),
|
|
82
|
+
expires_at: new Date(Date.now() + 60000).toISOString(), // 1 minute from now
|
|
83
|
+
status: 'pending',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
|
|
87
|
+
{ id: 'request-123', data: mockRequest }
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const result = await service.validateToken(mockUserId, mockToken);
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
...mockRequest,
|
|
94
|
+
request_id: 'request-123',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return null if token not found', async () => {
|
|
99
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
|
|
100
|
+
|
|
101
|
+
const result = await service.validateToken(mockUserId, mockToken);
|
|
102
|
+
|
|
103
|
+
expect(result).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return null and mark expired if token is expired', async () => {
|
|
107
|
+
const mockRequest: ConfirmationRequest = {
|
|
108
|
+
user_id: mockUserId,
|
|
109
|
+
token: mockToken,
|
|
110
|
+
action: 'publish_memory',
|
|
111
|
+
payload: {},
|
|
112
|
+
created_at: new Date(Date.now() - 10 * 60 * 1000).toISOString(), // 10 minutes ago
|
|
113
|
+
expires_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(), // 5 minutes ago (expired)
|
|
114
|
+
status: 'pending',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
|
|
118
|
+
{ id: 'request-123', data: mockRequest }
|
|
119
|
+
]);
|
|
120
|
+
(firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
|
|
121
|
+
|
|
122
|
+
const result = await service.validateToken(mockUserId, mockToken);
|
|
123
|
+
|
|
124
|
+
expect(result).toBeNull();
|
|
125
|
+
expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
|
|
126
|
+
'users/test-user-123/requests',
|
|
127
|
+
'request-123',
|
|
128
|
+
{ status: 'expired' }
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('confirmRequest', () => {
|
|
134
|
+
it('should confirm a valid request', async () => {
|
|
135
|
+
const mockRequest: ConfirmationRequest = {
|
|
136
|
+
user_id: mockUserId,
|
|
137
|
+
token: mockToken,
|
|
138
|
+
action: 'publish_memory',
|
|
139
|
+
payload: { memory_id: 'mem-123' },
|
|
140
|
+
created_at: new Date().toISOString(),
|
|
141
|
+
expires_at: new Date(Date.now() + 60000).toISOString(),
|
|
142
|
+
status: 'pending',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
|
|
146
|
+
{ id: 'request-123', data: mockRequest }
|
|
147
|
+
]);
|
|
148
|
+
(firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
|
|
149
|
+
|
|
150
|
+
const result = await service.confirmRequest(mockUserId, mockToken);
|
|
151
|
+
|
|
152
|
+
expect(result).not.toBeNull();
|
|
153
|
+
expect(result?.status).toBe('confirmed');
|
|
154
|
+
expect(result?.confirmed_at).toBeDefined();
|
|
155
|
+
expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
|
|
156
|
+
'users/test-user-123/requests',
|
|
157
|
+
'request-123',
|
|
158
|
+
expect.objectContaining({
|
|
159
|
+
status: 'confirmed',
|
|
160
|
+
confirmed_at: expect.any(String),
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return null if token is invalid', async () => {
|
|
166
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
|
|
167
|
+
|
|
168
|
+
const result = await service.confirmRequest(mockUserId, mockToken);
|
|
169
|
+
|
|
170
|
+
expect(result).toBeNull();
|
|
171
|
+
expect(firestoreInit.updateDocument).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('denyRequest', () => {
|
|
176
|
+
it('should deny a valid request', async () => {
|
|
177
|
+
const mockRequest: ConfirmationRequest = {
|
|
178
|
+
user_id: mockUserId,
|
|
179
|
+
token: mockToken,
|
|
180
|
+
action: 'publish_memory',
|
|
181
|
+
payload: {},
|
|
182
|
+
created_at: new Date().toISOString(),
|
|
183
|
+
expires_at: new Date(Date.now() + 60000).toISOString(),
|
|
184
|
+
status: 'pending',
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
|
|
188
|
+
{ id: 'request-123', data: mockRequest }
|
|
189
|
+
]);
|
|
190
|
+
(firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
|
|
191
|
+
|
|
192
|
+
const result = await service.denyRequest(mockUserId, mockToken);
|
|
193
|
+
|
|
194
|
+
expect(result).toBe(true);
|
|
195
|
+
expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
|
|
196
|
+
'users/test-user-123/requests',
|
|
197
|
+
'request-123',
|
|
198
|
+
{ status: 'denied' }
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should return false if token is invalid', async () => {
|
|
203
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
|
|
204
|
+
|
|
205
|
+
const result = await service.denyRequest(mockUserId, mockToken);
|
|
206
|
+
|
|
207
|
+
expect(result).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('retractRequest', () => {
|
|
212
|
+
it('should retract a valid request', async () => {
|
|
213
|
+
const mockRequest: ConfirmationRequest = {
|
|
214
|
+
user_id: mockUserId,
|
|
215
|
+
token: mockToken,
|
|
216
|
+
action: 'publish_memory',
|
|
217
|
+
payload: {},
|
|
218
|
+
created_at: new Date().toISOString(),
|
|
219
|
+
expires_at: new Date(Date.now() + 60000).toISOString(),
|
|
220
|
+
status: 'pending',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
|
|
224
|
+
{ id: 'request-123', data: mockRequest }
|
|
225
|
+
]);
|
|
226
|
+
(firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
|
|
227
|
+
|
|
228
|
+
const result = await service.retractRequest(mockUserId, mockToken);
|
|
229
|
+
|
|
230
|
+
expect(result).toBe(true);
|
|
231
|
+
expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
|
|
232
|
+
'users/test-user-123/requests',
|
|
233
|
+
'request-123',
|
|
234
|
+
{ status: 'retracted' }
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should return false if token is invalid', async () => {
|
|
239
|
+
(firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
|
|
240
|
+
|
|
241
|
+
const result = await service.retractRequest(mockUserId, mockToken);
|
|
242
|
+
|
|
243
|
+
expect(result).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('cleanupExpired', () => {
|
|
248
|
+
it('should return 0 (not implemented - relies on Firestore TTL)', async () => {
|
|
249
|
+
const result = await service.cleanupExpired();
|
|
250
|
+
|
|
251
|
+
expect(result).toBe(0);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirmation Token Service
|
|
3
|
+
*
|
|
4
|
+
* Manages confirmation tokens for sensitive operations like publishing memories.
|
|
5
|
+
* Tokens are one-time use with 5-minute expiry.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
import {
|
|
10
|
+
getDocument,
|
|
11
|
+
addDocument,
|
|
12
|
+
updateDocument,
|
|
13
|
+
queryDocuments,
|
|
14
|
+
type QueryOptions
|
|
15
|
+
} from '../firestore/init.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Confirmation request stored in Firestore
|
|
19
|
+
*/
|
|
20
|
+
export interface ConfirmationRequest {
|
|
21
|
+
user_id: string;
|
|
22
|
+
token: string;
|
|
23
|
+
action: string;
|
|
24
|
+
target_collection?: string;
|
|
25
|
+
payload: any;
|
|
26
|
+
created_at: string; // ISO 8601 timestamp
|
|
27
|
+
expires_at: string; // ISO 8601 timestamp
|
|
28
|
+
status: 'pending' | 'confirmed' | 'denied' | 'expired' | 'retracted';
|
|
29
|
+
confirmed_at?: string; // ISO 8601 timestamp
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Service for managing confirmation tokens
|
|
34
|
+
*/
|
|
35
|
+
export class ConfirmationTokenService {
|
|
36
|
+
private readonly EXPIRY_MINUTES = 5;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a new confirmation request
|
|
40
|
+
*
|
|
41
|
+
* @param userId - User ID who initiated the request
|
|
42
|
+
* @param action - Action type (e.g., 'publish_memory')
|
|
43
|
+
* @param payload - Data to store with the request
|
|
44
|
+
* @param targetCollection - Optional target collection (e.g., 'the_void')
|
|
45
|
+
* @returns Request ID and token
|
|
46
|
+
*/
|
|
47
|
+
async createRequest(
|
|
48
|
+
userId: string,
|
|
49
|
+
action: string,
|
|
50
|
+
payload: any,
|
|
51
|
+
targetCollection?: string
|
|
52
|
+
): Promise<{ requestId: string; token: string }> {
|
|
53
|
+
const token = randomUUID();
|
|
54
|
+
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const expiresAt = new Date(now.getTime() + this.EXPIRY_MINUTES * 60 * 1000);
|
|
57
|
+
|
|
58
|
+
const request: ConfirmationRequest = {
|
|
59
|
+
user_id: userId,
|
|
60
|
+
token,
|
|
61
|
+
action,
|
|
62
|
+
target_collection: targetCollection,
|
|
63
|
+
payload,
|
|
64
|
+
created_at: now.toISOString(),
|
|
65
|
+
expires_at: expiresAt.toISOString(),
|
|
66
|
+
status: 'pending',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Add document to Firestore (auto-generates ID)
|
|
70
|
+
const collectionPath = `users/${userId}/requests`;
|
|
71
|
+
console.log('[ConfirmationTokenService] Creating request:', {
|
|
72
|
+
userId,
|
|
73
|
+
action,
|
|
74
|
+
targetCollection,
|
|
75
|
+
collectionPath,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const docRef = await addDocument(collectionPath, request);
|
|
79
|
+
|
|
80
|
+
console.log('[ConfirmationTokenService] Request created:', {
|
|
81
|
+
requestId: docRef.id,
|
|
82
|
+
token,
|
|
83
|
+
expiresAt: request.expires_at,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return { requestId: docRef.id, token };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate and retrieve a confirmation request
|
|
91
|
+
*
|
|
92
|
+
* @param userId - User ID
|
|
93
|
+
* @param token - Confirmation token
|
|
94
|
+
* @returns Request with request_id if valid, null otherwise
|
|
95
|
+
*/
|
|
96
|
+
async validateToken(
|
|
97
|
+
userId: string,
|
|
98
|
+
token: string
|
|
99
|
+
): Promise<(ConfirmationRequest & { request_id: string }) | null> {
|
|
100
|
+
const collectionPath = `users/${userId}/requests`;
|
|
101
|
+
|
|
102
|
+
console.log('[ConfirmationTokenService] Validating token:', {
|
|
103
|
+
userId,
|
|
104
|
+
token,
|
|
105
|
+
collectionPath,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Query for the token
|
|
109
|
+
const queryOptions: QueryOptions = {
|
|
110
|
+
where: [
|
|
111
|
+
{ field: 'token', op: '==', value: token },
|
|
112
|
+
{ field: 'status', op: '==', value: 'pending' },
|
|
113
|
+
],
|
|
114
|
+
limit: 1,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const results = await queryDocuments(collectionPath, queryOptions);
|
|
118
|
+
|
|
119
|
+
console.log('[ConfirmationTokenService] Query results:', {
|
|
120
|
+
resultsFound: results.length,
|
|
121
|
+
hasResults: results.length > 0,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (results.length === 0) {
|
|
125
|
+
console.log('[ConfirmationTokenService] Token not found or not pending');
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const doc = results[0];
|
|
130
|
+
const request = doc.data as ConfirmationRequest;
|
|
131
|
+
|
|
132
|
+
console.log('[ConfirmationTokenService] Request found:', {
|
|
133
|
+
requestId: doc.id,
|
|
134
|
+
action: request.action,
|
|
135
|
+
status: request.status,
|
|
136
|
+
expiresAt: request.expires_at,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Check expiry
|
|
140
|
+
const expiresAt = new Date(request.expires_at);
|
|
141
|
+
if (expiresAt.getTime() < Date.now()) {
|
|
142
|
+
console.log('[ConfirmationTokenService] Token expired');
|
|
143
|
+
await this.updateStatus(userId, doc.id, 'expired');
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
...request,
|
|
149
|
+
request_id: doc.id,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Confirm a request
|
|
155
|
+
*
|
|
156
|
+
* @param userId - User ID
|
|
157
|
+
* @param token - Confirmation token
|
|
158
|
+
* @returns Confirmed request if valid, null otherwise
|
|
159
|
+
*/
|
|
160
|
+
async confirmRequest(
|
|
161
|
+
userId: string,
|
|
162
|
+
token: string
|
|
163
|
+
): Promise<(ConfirmationRequest & { request_id: string }) | null> {
|
|
164
|
+
const request = await this.validateToken(userId, token);
|
|
165
|
+
if (!request) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await this.updateStatus(userId, request.request_id, 'confirmed');
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...request,
|
|
173
|
+
status: 'confirmed',
|
|
174
|
+
confirmed_at: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Deny a request
|
|
180
|
+
*
|
|
181
|
+
* @param userId - User ID
|
|
182
|
+
* @param token - Confirmation token
|
|
183
|
+
* @returns True if denied successfully, false otherwise
|
|
184
|
+
*/
|
|
185
|
+
async denyRequest(
|
|
186
|
+
userId: string,
|
|
187
|
+
token: string
|
|
188
|
+
): Promise<boolean> {
|
|
189
|
+
const request = await this.validateToken(userId, token);
|
|
190
|
+
if (!request) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await this.updateStatus(userId, request.request_id, 'denied');
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Retract a request
|
|
200
|
+
*
|
|
201
|
+
* @param userId - User ID
|
|
202
|
+
* @param token - Confirmation token
|
|
203
|
+
* @returns True if retracted successfully, false otherwise
|
|
204
|
+
*/
|
|
205
|
+
async retractRequest(
|
|
206
|
+
userId: string,
|
|
207
|
+
token: string
|
|
208
|
+
): Promise<boolean> {
|
|
209
|
+
const request = await this.validateToken(userId, token);
|
|
210
|
+
if (!request) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await this.updateStatus(userId, request.request_id, 'retracted');
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Update request status
|
|
220
|
+
*
|
|
221
|
+
* @param userId - User ID
|
|
222
|
+
* @param requestId - Request document ID
|
|
223
|
+
* @param status - New status
|
|
224
|
+
*/
|
|
225
|
+
private async updateStatus(
|
|
226
|
+
userId: string,
|
|
227
|
+
requestId: string,
|
|
228
|
+
status: ConfirmationRequest['status']
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
const collectionPath = `users/${userId}/requests`;
|
|
231
|
+
|
|
232
|
+
const updateData: Partial<ConfirmationRequest> = {
|
|
233
|
+
status,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (status === 'confirmed') {
|
|
237
|
+
updateData.confirmed_at = new Date().toISOString();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await updateDocument(collectionPath, requestId, updateData);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Clean up expired requests (optional - Firestore TTL handles deletion)
|
|
245
|
+
*
|
|
246
|
+
* Note: Configure Firestore TTL policy on 'requests' collection group
|
|
247
|
+
* with 'expires_at' field for automatic deletion within 24 hours.
|
|
248
|
+
*
|
|
249
|
+
* This method is optional for immediate cleanup if needed.
|
|
250
|
+
*
|
|
251
|
+
* @returns Count of deleted requests
|
|
252
|
+
*/
|
|
253
|
+
async cleanupExpired(): Promise<number> {
|
|
254
|
+
// Note: firebase-admin-sdk-v8 doesn't support collectionGroup queries
|
|
255
|
+
// This would need to be implemented differently or rely on Firestore TTL
|
|
256
|
+
// For now, return 0 and rely on Firestore TTL policy
|
|
257
|
+
console.warn('[ConfirmationTokenService] cleanupExpired not implemented - rely on Firestore TTL');
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Singleton instance of the confirmation token service
|
|
264
|
+
*/
|
|
265
|
+
export const confirmationTokenService = new ConfirmationTokenService();
|