@affectively/aeon-pages 1.3.0
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/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Pages Offline Encryption
|
|
3
|
+
*
|
|
4
|
+
* UCAN-based encryption wrapper for queued operations.
|
|
5
|
+
* Uses Web Crypto API exclusively - no external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Key Derivation:
|
|
8
|
+
* 1. UCAN signing key or session key as base material
|
|
9
|
+
* 2. Derive encryption key via HKDF with context string
|
|
10
|
+
* 3. Each operation encrypted with AES-256-GCM + authenticated
|
|
11
|
+
* 4. Nonce is random per operation (prevents replay)
|
|
12
|
+
*
|
|
13
|
+
* Security Properties:
|
|
14
|
+
* - Forward secrecy: Each operation uses unique nonce
|
|
15
|
+
* - Authentication: GCM provides built-in authentication tag
|
|
16
|
+
* - Key binding: Operations tied to user's key material
|
|
17
|
+
* - No plaintext: Operations never stored unencrypted
|
|
18
|
+
*
|
|
19
|
+
* Binary Format: [version:1][nonce:12][ciphertext:*][auth_tag:16]
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
OfflineOperation,
|
|
24
|
+
EncryptedPayload,
|
|
25
|
+
EncryptionKeyMaterial,
|
|
26
|
+
} from './types';
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Constants
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
const ENCRYPTION_VERSION = 1;
|
|
33
|
+
const NONCE_LENGTH = 12; // 96 bits for AES-GCM
|
|
34
|
+
const TAG_LENGTH = 16; // 128 bits auth tag
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Encryption Service
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Offline operation encryption service
|
|
42
|
+
* Uses Web Crypto API for all cryptographic operations
|
|
43
|
+
*/
|
|
44
|
+
export class OfflineOperationEncryption {
|
|
45
|
+
private keyCache: Map<string, EncryptionKeyMaterial> = new Map();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Derive an encryption key from UCAN signing key material
|
|
49
|
+
*/
|
|
50
|
+
async deriveKeyFromUCAN(
|
|
51
|
+
userId: string,
|
|
52
|
+
signingKeyBytes: Uint8Array,
|
|
53
|
+
context: string,
|
|
54
|
+
): Promise<EncryptionKeyMaterial> {
|
|
55
|
+
const cacheKey = `${userId}:${context}`;
|
|
56
|
+
|
|
57
|
+
// Check cache first
|
|
58
|
+
if (this.keyCache.has(cacheKey)) {
|
|
59
|
+
return this.keyCache.get(cacheKey)!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Import the signing key material as HKDF base key
|
|
63
|
+
const baseKey = await crypto.subtle.importKey(
|
|
64
|
+
'raw',
|
|
65
|
+
signingKeyBytes.buffer as ArrayBuffer,
|
|
66
|
+
'HKDF',
|
|
67
|
+
false,
|
|
68
|
+
['deriveKey'],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Derive AES-256-GCM key using HKDF
|
|
72
|
+
const info = new TextEncoder().encode(`aeon-offline-operation:${context}`);
|
|
73
|
+
const salt = new TextEncoder().encode('aeon-pages-v1');
|
|
74
|
+
|
|
75
|
+
const encryptionKey = await crypto.subtle.deriveKey(
|
|
76
|
+
{
|
|
77
|
+
name: 'HKDF',
|
|
78
|
+
hash: 'SHA-256',
|
|
79
|
+
salt,
|
|
80
|
+
info,
|
|
81
|
+
},
|
|
82
|
+
baseKey,
|
|
83
|
+
{ name: 'AES-GCM', length: 256 },
|
|
84
|
+
false,
|
|
85
|
+
['encrypt', 'decrypt'],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const material: EncryptionKeyMaterial = {
|
|
89
|
+
key: encryptionKey,
|
|
90
|
+
context,
|
|
91
|
+
userId,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.keyCache.set(cacheKey, material);
|
|
95
|
+
return material;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Derive an encryption key from session-based material (fallback when UCAN not available)
|
|
100
|
+
*/
|
|
101
|
+
async deriveKeyFromSession(
|
|
102
|
+
sessionId: string,
|
|
103
|
+
context: string,
|
|
104
|
+
): Promise<EncryptionKeyMaterial> {
|
|
105
|
+
const cacheKey = `session:${sessionId}:${context}`;
|
|
106
|
+
|
|
107
|
+
if (this.keyCache.has(cacheKey)) {
|
|
108
|
+
return this.keyCache.get(cacheKey)!;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Generate deterministic key from session ID
|
|
112
|
+
const sessionBytes = new TextEncoder().encode(sessionId);
|
|
113
|
+
const baseKey = await crypto.subtle.importKey(
|
|
114
|
+
'raw',
|
|
115
|
+
sessionBytes,
|
|
116
|
+
'HKDF',
|
|
117
|
+
false,
|
|
118
|
+
['deriveKey'],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const info = new TextEncoder().encode(`aeon-session-operation:${context}`);
|
|
122
|
+
const salt = new TextEncoder().encode('aeon-pages-session-v1');
|
|
123
|
+
|
|
124
|
+
const encryptionKey = await crypto.subtle.deriveKey(
|
|
125
|
+
{
|
|
126
|
+
name: 'HKDF',
|
|
127
|
+
hash: 'SHA-256',
|
|
128
|
+
salt,
|
|
129
|
+
info,
|
|
130
|
+
},
|
|
131
|
+
baseKey,
|
|
132
|
+
{ name: 'AES-GCM', length: 256 },
|
|
133
|
+
false,
|
|
134
|
+
['encrypt', 'decrypt'],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const material: EncryptionKeyMaterial = {
|
|
138
|
+
key: encryptionKey,
|
|
139
|
+
context,
|
|
140
|
+
userId: sessionId,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.keyCache.set(cacheKey, material);
|
|
144
|
+
return material;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Encrypt operation for queue storage
|
|
149
|
+
* Returns binary format: [version:1][nonce:12][ciphertext+tag]
|
|
150
|
+
*/
|
|
151
|
+
async encryptOperation(
|
|
152
|
+
operation: Omit<
|
|
153
|
+
OfflineOperation,
|
|
154
|
+
| 'id'
|
|
155
|
+
| 'status'
|
|
156
|
+
| 'encryptedData'
|
|
157
|
+
| 'bytesSize'
|
|
158
|
+
| 'failedCount'
|
|
159
|
+
| 'retryCount'
|
|
160
|
+
| 'maxRetries'
|
|
161
|
+
>,
|
|
162
|
+
keyMaterial: EncryptionKeyMaterial,
|
|
163
|
+
): Promise<Uint8Array> {
|
|
164
|
+
// Serialize operation to JSON
|
|
165
|
+
const operationJson = JSON.stringify({
|
|
166
|
+
type: operation.type,
|
|
167
|
+
sessionId: operation.sessionId,
|
|
168
|
+
data: operation.data,
|
|
169
|
+
priority: operation.priority,
|
|
170
|
+
createdAt: operation.createdAt,
|
|
171
|
+
encryptionVersion: operation.encryptionVersion,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const plaintext = new TextEncoder().encode(operationJson);
|
|
175
|
+
|
|
176
|
+
// Generate random nonce
|
|
177
|
+
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
|
178
|
+
|
|
179
|
+
// Encrypt with AES-GCM (includes authentication tag)
|
|
180
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
181
|
+
{
|
|
182
|
+
name: 'AES-GCM',
|
|
183
|
+
iv: nonce,
|
|
184
|
+
tagLength: TAG_LENGTH * 8, // bits
|
|
185
|
+
},
|
|
186
|
+
keyMaterial.key,
|
|
187
|
+
plaintext,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Serialize to binary: [version:1][nonce:12][ciphertext+tag]
|
|
191
|
+
const ciphertextBytes = new Uint8Array(ciphertext);
|
|
192
|
+
const serialized = new Uint8Array(
|
|
193
|
+
1 + NONCE_LENGTH + ciphertextBytes.length,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
serialized[0] = ENCRYPTION_VERSION;
|
|
197
|
+
serialized.set(nonce, 1);
|
|
198
|
+
serialized.set(ciphertextBytes, 1 + NONCE_LENGTH);
|
|
199
|
+
|
|
200
|
+
return serialized;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Decrypt operation from queue storage
|
|
205
|
+
*/
|
|
206
|
+
async decryptOperation(
|
|
207
|
+
encryptedData: Uint8Array,
|
|
208
|
+
keyMaterial: EncryptionKeyMaterial,
|
|
209
|
+
): Promise<
|
|
210
|
+
Omit<
|
|
211
|
+
OfflineOperation,
|
|
212
|
+
| 'id'
|
|
213
|
+
| 'status'
|
|
214
|
+
| 'encryptedData'
|
|
215
|
+
| 'bytesSize'
|
|
216
|
+
| 'failedCount'
|
|
217
|
+
| 'retryCount'
|
|
218
|
+
| 'maxRetries'
|
|
219
|
+
>
|
|
220
|
+
> {
|
|
221
|
+
// Verify version
|
|
222
|
+
const version = encryptedData[0];
|
|
223
|
+
if (version !== ENCRYPTION_VERSION) {
|
|
224
|
+
throw new Error(`Unsupported encryption version: ${version}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Extract nonce and ciphertext
|
|
228
|
+
const nonce = encryptedData.slice(1, 1 + NONCE_LENGTH);
|
|
229
|
+
const ciphertext = encryptedData.slice(1 + NONCE_LENGTH);
|
|
230
|
+
|
|
231
|
+
// Decrypt with AES-GCM
|
|
232
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
233
|
+
{
|
|
234
|
+
name: 'AES-GCM',
|
|
235
|
+
iv: nonce,
|
|
236
|
+
tagLength: TAG_LENGTH * 8,
|
|
237
|
+
},
|
|
238
|
+
keyMaterial.key,
|
|
239
|
+
ciphertext,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Parse JSON
|
|
243
|
+
const operationJson = new TextDecoder().decode(plaintext);
|
|
244
|
+
const parsed = JSON.parse(operationJson);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
type: parsed.type,
|
|
248
|
+
sessionId: parsed.sessionId,
|
|
249
|
+
data: parsed.data,
|
|
250
|
+
priority: parsed.priority || 'normal',
|
|
251
|
+
createdAt: parsed.createdAt || Date.now(),
|
|
252
|
+
encryptionVersion: parsed.encryptionVersion || ENCRYPTION_VERSION,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Encrypt a batch of operations for sync payload
|
|
258
|
+
*/
|
|
259
|
+
async encryptSyncBatch(
|
|
260
|
+
operations: Array<{
|
|
261
|
+
operationId: string;
|
|
262
|
+
sessionId: string;
|
|
263
|
+
type: string;
|
|
264
|
+
data: Record<string, unknown>;
|
|
265
|
+
}>,
|
|
266
|
+
keyMaterial: EncryptionKeyMaterial,
|
|
267
|
+
): Promise<EncryptedPayload> {
|
|
268
|
+
const batchJson = JSON.stringify({
|
|
269
|
+
operations,
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
userId: keyMaterial.userId,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const plaintext = new TextEncoder().encode(batchJson);
|
|
275
|
+
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
|
276
|
+
|
|
277
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
278
|
+
{
|
|
279
|
+
name: 'AES-GCM',
|
|
280
|
+
iv: nonce,
|
|
281
|
+
tagLength: TAG_LENGTH * 8,
|
|
282
|
+
},
|
|
283
|
+
keyMaterial.key,
|
|
284
|
+
plaintext,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
version: ENCRYPTION_VERSION,
|
|
289
|
+
nonce,
|
|
290
|
+
ciphertext: new Uint8Array(ciphertext),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Decrypt a batch of operations from sync response
|
|
296
|
+
*/
|
|
297
|
+
async decryptSyncBatch(
|
|
298
|
+
encrypted: EncryptedPayload,
|
|
299
|
+
keyMaterial: EncryptionKeyMaterial,
|
|
300
|
+
): Promise<
|
|
301
|
+
Array<{
|
|
302
|
+
operationId: string;
|
|
303
|
+
sessionId: string;
|
|
304
|
+
type: string;
|
|
305
|
+
data: Record<string, unknown>;
|
|
306
|
+
}>
|
|
307
|
+
> {
|
|
308
|
+
if (encrypted.version !== ENCRYPTION_VERSION) {
|
|
309
|
+
throw new Error(`Unsupported encryption version: ${encrypted.version}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
313
|
+
{
|
|
314
|
+
name: 'AES-GCM',
|
|
315
|
+
iv: encrypted.nonce.buffer as ArrayBuffer,
|
|
316
|
+
tagLength: TAG_LENGTH * 8,
|
|
317
|
+
},
|
|
318
|
+
keyMaterial.key,
|
|
319
|
+
encrypted.ciphertext.buffer as ArrayBuffer,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const batchJson = new TextDecoder().decode(plaintext);
|
|
323
|
+
const parsed = JSON.parse(batchJson);
|
|
324
|
+
|
|
325
|
+
return parsed.operations;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Clear the key cache (call on logout/session end)
|
|
330
|
+
*/
|
|
331
|
+
clearKeyCache(): void {
|
|
332
|
+
this.keyCache.clear();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Remove a specific key from cache
|
|
337
|
+
*/
|
|
338
|
+
removeKeyFromCache(userId: string, context: string): void {
|
|
339
|
+
// Try both UCAN and session key formats
|
|
340
|
+
this.keyCache.delete(`${userId}:${context}`);
|
|
341
|
+
this.keyCache.delete(`session:${userId}:${context}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ============================================================================
|
|
346
|
+
// Singleton Instance
|
|
347
|
+
// ============================================================================
|
|
348
|
+
|
|
349
|
+
let _instance: OfflineOperationEncryption | null = null;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get the singleton encryption service instance
|
|
353
|
+
*/
|
|
354
|
+
export function getOperationEncryption(): OfflineOperationEncryption {
|
|
355
|
+
if (!_instance) {
|
|
356
|
+
_instance = new OfflineOperationEncryption();
|
|
357
|
+
}
|
|
358
|
+
return _instance;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Reset the encryption service (for testing)
|
|
363
|
+
*/
|
|
364
|
+
export function resetOperationEncryption(): void {
|
|
365
|
+
if (_instance) {
|
|
366
|
+
_instance.clearKeyCache();
|
|
367
|
+
}
|
|
368
|
+
_instance = null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ============================================================================
|
|
372
|
+
// Utility Functions
|
|
373
|
+
// ============================================================================
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Generate a random operation ID
|
|
377
|
+
*/
|
|
378
|
+
export function generateOperationId(): string {
|
|
379
|
+
const timestamp = Date.now().toString(36);
|
|
380
|
+
const random = crypto.getRandomValues(new Uint8Array(8));
|
|
381
|
+
const randomStr = Array.from(random)
|
|
382
|
+
.map((b) => b.toString(36).padStart(2, '0'))
|
|
383
|
+
.join('')
|
|
384
|
+
.slice(0, 9);
|
|
385
|
+
return `op_${timestamp}_${randomStr}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Estimate the encrypted size of an operation
|
|
390
|
+
*/
|
|
391
|
+
export function estimateEncryptedSize(
|
|
392
|
+
operation: Record<string, unknown>,
|
|
393
|
+
): number {
|
|
394
|
+
const json = JSON.stringify(operation);
|
|
395
|
+
// JSON size + version byte + nonce + auth tag + some padding
|
|
396
|
+
return json.length + 1 + NONCE_LENGTH + TAG_LENGTH + 16;
|
|
397
|
+
}
|