@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. 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
+ }