@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,412 @@
1
+ /**
2
+ * Tests for Offline Operation Encryption
3
+ */
4
+
5
+ import { describe, test, expect, beforeEach } from 'bun:test';
6
+ import {
7
+ OfflineOperationEncryption,
8
+ getOperationEncryption,
9
+ resetOperationEncryption,
10
+ generateOperationId,
11
+ estimateEncryptedSize,
12
+ } from './encryption';
13
+
14
+ describe('OfflineOperationEncryption', () => {
15
+ let encryption: OfflineOperationEncryption;
16
+
17
+ beforeEach(() => {
18
+ resetOperationEncryption();
19
+ encryption = new OfflineOperationEncryption();
20
+ });
21
+
22
+ describe('deriveKeyFromSession', () => {
23
+ test('derives a key from session ID', async () => {
24
+ const keyMaterial = await encryption.deriveKeyFromSession(
25
+ 'session-123',
26
+ 'test-context',
27
+ );
28
+
29
+ expect(keyMaterial).toBeDefined();
30
+ expect(keyMaterial.key).toBeInstanceOf(CryptoKey);
31
+ expect(keyMaterial.context).toBe('test-context');
32
+ expect(keyMaterial.userId).toBe('session-123');
33
+ });
34
+
35
+ test('caches derived keys', async () => {
36
+ const key1 = await encryption.deriveKeyFromSession(
37
+ 'session-123',
38
+ 'test-context',
39
+ );
40
+ const key2 = await encryption.deriveKeyFromSession(
41
+ 'session-123',
42
+ 'test-context',
43
+ );
44
+
45
+ // Should return the same cached key
46
+ expect(key1).toBe(key2);
47
+ });
48
+
49
+ test('derives different keys for different sessions', async () => {
50
+ const key1 = await encryption.deriveKeyFromSession(
51
+ 'session-1',
52
+ 'test-context',
53
+ );
54
+ const key2 = await encryption.deriveKeyFromSession(
55
+ 'session-2',
56
+ 'test-context',
57
+ );
58
+
59
+ expect(key1.userId).not.toBe(key2.userId);
60
+ });
61
+
62
+ test('derives different keys for different contexts', async () => {
63
+ const key1 = await encryption.deriveKeyFromSession(
64
+ 'session-123',
65
+ 'context-1',
66
+ );
67
+ const key2 = await encryption.deriveKeyFromSession(
68
+ 'session-123',
69
+ 'context-2',
70
+ );
71
+
72
+ expect(key1.context).not.toBe(key2.context);
73
+ });
74
+ });
75
+
76
+ describe('deriveKeyFromUCAN', () => {
77
+ test('derives a key from UCAN signing key bytes', async () => {
78
+ const signingKeyBytes = crypto.getRandomValues(new Uint8Array(32));
79
+ const keyMaterial = await encryption.deriveKeyFromUCAN(
80
+ 'user-123',
81
+ signingKeyBytes,
82
+ 'ucan-context',
83
+ );
84
+
85
+ expect(keyMaterial).toBeDefined();
86
+ expect(keyMaterial.key).toBeInstanceOf(CryptoKey);
87
+ expect(keyMaterial.context).toBe('ucan-context');
88
+ expect(keyMaterial.userId).toBe('user-123');
89
+ });
90
+
91
+ test('caches UCAN-derived keys', async () => {
92
+ const signingKeyBytes = crypto.getRandomValues(new Uint8Array(32));
93
+ const key1 = await encryption.deriveKeyFromUCAN(
94
+ 'user-123',
95
+ signingKeyBytes,
96
+ 'ucan-context',
97
+ );
98
+ const key2 = await encryption.deriveKeyFromUCAN(
99
+ 'user-123',
100
+ signingKeyBytes,
101
+ 'ucan-context',
102
+ );
103
+
104
+ expect(key1).toBe(key2);
105
+ });
106
+ });
107
+
108
+ describe('encryptOperation / decryptOperation', () => {
109
+ test('encrypts and decrypts an operation correctly', async () => {
110
+ const keyMaterial = await encryption.deriveKeyFromSession(
111
+ 'session-123',
112
+ 'test',
113
+ );
114
+
115
+ const operation = {
116
+ type: 'update' as const,
117
+ sessionId: 'session-123',
118
+ data: { foo: 'bar', count: 42 },
119
+ priority: 'normal' as const,
120
+ createdAt: Date.now(),
121
+ };
122
+
123
+ const encrypted = await encryption.encryptOperation(
124
+ operation,
125
+ keyMaterial,
126
+ );
127
+
128
+ expect(encrypted).toBeInstanceOf(Uint8Array);
129
+ expect(encrypted.length).toBeGreaterThan(0);
130
+ // First byte should be version
131
+ expect(encrypted[0]).toBe(1);
132
+
133
+ const decrypted = await encryption.decryptOperation(
134
+ encrypted,
135
+ keyMaterial,
136
+ );
137
+
138
+ expect(decrypted.type).toBe(operation.type);
139
+ expect(decrypted.sessionId).toBe(operation.sessionId);
140
+ expect(decrypted.data).toEqual(operation.data);
141
+ expect(decrypted.priority).toBe(operation.priority);
142
+ });
143
+
144
+ test('encrypted data differs for same plaintext (random nonce)', async () => {
145
+ const keyMaterial = await encryption.deriveKeyFromSession(
146
+ 'session-123',
147
+ 'test',
148
+ );
149
+
150
+ const operation = {
151
+ type: 'update' as const,
152
+ sessionId: 'session-123',
153
+ data: { foo: 'bar' },
154
+ priority: 'normal' as const,
155
+ createdAt: Date.now(),
156
+ };
157
+
158
+ const encrypted1 = await encryption.encryptOperation(
159
+ operation,
160
+ keyMaterial,
161
+ );
162
+ const encrypted2 = await encryption.encryptOperation(
163
+ operation,
164
+ keyMaterial,
165
+ );
166
+
167
+ // Encrypted data should differ due to random nonce
168
+ expect(encrypted1).not.toEqual(encrypted2);
169
+ });
170
+
171
+ test('fails to decrypt with wrong key', async () => {
172
+ const keyMaterial1 = await encryption.deriveKeyFromSession(
173
+ 'session-1',
174
+ 'test',
175
+ );
176
+ const keyMaterial2 = await encryption.deriveKeyFromSession(
177
+ 'session-2',
178
+ 'test',
179
+ );
180
+
181
+ const operation = {
182
+ type: 'update' as const,
183
+ sessionId: 'session-1',
184
+ data: { secret: 'data' },
185
+ priority: 'normal' as const,
186
+ createdAt: Date.now(),
187
+ };
188
+
189
+ const encrypted = await encryption.encryptOperation(
190
+ operation,
191
+ keyMaterial1,
192
+ );
193
+
194
+ await expect(
195
+ encryption.decryptOperation(encrypted, keyMaterial2),
196
+ ).rejects.toThrow();
197
+ });
198
+
199
+ test('fails to decrypt tampered data', async () => {
200
+ const keyMaterial = await encryption.deriveKeyFromSession(
201
+ 'session-123',
202
+ 'test',
203
+ );
204
+
205
+ const operation = {
206
+ type: 'update' as const,
207
+ sessionId: 'session-123',
208
+ data: { foo: 'bar' },
209
+ priority: 'normal' as const,
210
+ createdAt: Date.now(),
211
+ };
212
+
213
+ const encrypted = await encryption.encryptOperation(
214
+ operation,
215
+ keyMaterial,
216
+ );
217
+
218
+ // Tamper with the ciphertext
219
+ encrypted[20] ^= 0xff;
220
+
221
+ await expect(
222
+ encryption.decryptOperation(encrypted, keyMaterial),
223
+ ).rejects.toThrow();
224
+ });
225
+
226
+ test('rejects unsupported encryption version', async () => {
227
+ const keyMaterial = await encryption.deriveKeyFromSession(
228
+ 'session-123',
229
+ 'test',
230
+ );
231
+
232
+ const operation = {
233
+ type: 'update' as const,
234
+ sessionId: 'session-123',
235
+ data: { foo: 'bar' },
236
+ priority: 'normal' as const,
237
+ createdAt: Date.now(),
238
+ };
239
+
240
+ const encrypted = await encryption.encryptOperation(
241
+ operation,
242
+ keyMaterial,
243
+ );
244
+
245
+ // Change version byte to unsupported version
246
+ encrypted[0] = 99;
247
+
248
+ await expect(
249
+ encryption.decryptOperation(encrypted, keyMaterial),
250
+ ).rejects.toThrow('Unsupported encryption version: 99');
251
+ });
252
+ });
253
+
254
+ describe('encryptSyncBatch / decryptSyncBatch', () => {
255
+ test('encrypts and decrypts a batch of operations', async () => {
256
+ const keyMaterial = await encryption.deriveKeyFromSession(
257
+ 'session-123',
258
+ 'batch-test',
259
+ );
260
+
261
+ const operations = [
262
+ {
263
+ operationId: 'op-1',
264
+ sessionId: 'session-123',
265
+ type: 'create',
266
+ data: { name: 'test1' },
267
+ },
268
+ {
269
+ operationId: 'op-2',
270
+ sessionId: 'session-123',
271
+ type: 'update',
272
+ data: { name: 'test2' },
273
+ },
274
+ {
275
+ operationId: 'op-3',
276
+ sessionId: 'session-123',
277
+ type: 'delete',
278
+ data: { id: '123' },
279
+ },
280
+ ];
281
+
282
+ const encrypted = await encryption.encryptSyncBatch(
283
+ operations,
284
+ keyMaterial,
285
+ );
286
+
287
+ expect(encrypted.version).toBe(1);
288
+ expect(encrypted.nonce).toBeInstanceOf(Uint8Array);
289
+ expect(encrypted.nonce.length).toBe(12);
290
+ expect(encrypted.ciphertext).toBeInstanceOf(Uint8Array);
291
+
292
+ const decrypted = await encryption.decryptSyncBatch(
293
+ encrypted,
294
+ keyMaterial,
295
+ );
296
+
297
+ expect(decrypted).toHaveLength(3);
298
+ expect(decrypted[0].operationId).toBe('op-1');
299
+ expect(decrypted[1].operationId).toBe('op-2');
300
+ expect(decrypted[2].operationId).toBe('op-3');
301
+ });
302
+ });
303
+
304
+ describe('clearKeyCache', () => {
305
+ test('clears all cached keys', async () => {
306
+ await encryption.deriveKeyFromSession('session-1', 'context-1');
307
+ await encryption.deriveKeyFromSession('session-2', 'context-2');
308
+
309
+ encryption.clearKeyCache();
310
+
311
+ // After clearing, deriving the same key should create a new one
312
+ const newKey = await encryption.deriveKeyFromSession(
313
+ 'session-1',
314
+ 'context-1',
315
+ );
316
+ expect(newKey).toBeDefined();
317
+ });
318
+ });
319
+
320
+ describe('removeKeyFromCache', () => {
321
+ test('removes a specific key from cache', async () => {
322
+ const key1 = await encryption.deriveKeyFromSession(
323
+ 'session-1',
324
+ 'context-1',
325
+ );
326
+ await encryption.deriveKeyFromSession('session-2', 'context-2');
327
+
328
+ encryption.removeKeyFromCache('session-1', 'context-1');
329
+
330
+ // session-1 key should be regenerated
331
+ const newKey1 = await encryption.deriveKeyFromSession(
332
+ 'session-1',
333
+ 'context-1',
334
+ );
335
+ expect(newKey1).not.toBe(key1);
336
+ });
337
+ });
338
+ });
339
+
340
+ describe('getOperationEncryption', () => {
341
+ beforeEach(() => {
342
+ resetOperationEncryption();
343
+ });
344
+
345
+ test('returns singleton instance', () => {
346
+ const instance1 = getOperationEncryption();
347
+ const instance2 = getOperationEncryption();
348
+
349
+ expect(instance1).toBe(instance2);
350
+ });
351
+
352
+ test('resetOperationEncryption creates new instance', () => {
353
+ const instance1 = getOperationEncryption();
354
+ resetOperationEncryption();
355
+ const instance2 = getOperationEncryption();
356
+
357
+ expect(instance1).not.toBe(instance2);
358
+ });
359
+ });
360
+
361
+ describe('generateOperationId', () => {
362
+ test('generates unique IDs', () => {
363
+ const ids = new Set<string>();
364
+ for (let i = 0; i < 100; i++) {
365
+ ids.add(generateOperationId());
366
+ }
367
+ expect(ids.size).toBe(100);
368
+ });
369
+
370
+ test('generates IDs with correct prefix', () => {
371
+ const id = generateOperationId();
372
+ expect(id.startsWith('op_')).toBe(true);
373
+ });
374
+
375
+ test('generates IDs with timestamp component', () => {
376
+ const id = generateOperationId();
377
+ const parts = id.split('_');
378
+ expect(parts.length).toBe(3);
379
+ // Timestamp part should be valid base36
380
+ expect(() => parseInt(parts[1], 36)).not.toThrow();
381
+ });
382
+ });
383
+
384
+ describe('estimateEncryptedSize', () => {
385
+ test('estimates size for small operation', () => {
386
+ const operation = { foo: 'bar' };
387
+ const size = estimateEncryptedSize(operation);
388
+
389
+ // Should include JSON size + version + nonce + auth tag + padding
390
+ expect(size).toBeGreaterThan(JSON.stringify(operation).length);
391
+ });
392
+
393
+ test('estimates size for large operation', () => {
394
+ const operation = {
395
+ data: 'x'.repeat(1000),
396
+ nested: { a: 1, b: 2, c: 3 },
397
+ };
398
+ const size = estimateEncryptedSize(operation);
399
+
400
+ expect(size).toBeGreaterThan(1000);
401
+ });
402
+
403
+ test('size increases with operation complexity', () => {
404
+ const small = { a: 1 };
405
+ const large = { a: 1, b: 2, c: 3, d: 'long string value' };
406
+
407
+ const smallSize = estimateEncryptedSize(small);
408
+ const largeSize = estimateEncryptedSize(large);
409
+
410
+ expect(largeSize).toBeGreaterThan(smallSize);
411
+ });
412
+ });