@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,473 @@
1
+ /**
2
+ * Merkle-Based UCAN Capability Verification
3
+ *
4
+ * Provides fine-grained access control to component nodes using Merkle hashes.
5
+ * Integrates with UCAN tokens for cryptographic authorization.
6
+ *
7
+ * Resource formats:
8
+ * - `merkle:<hash>` - Exact match on Merkle hash
9
+ * - `tree:<hash>` - Match node or any ancestor with this hash
10
+ * - `path:<route>` - Match all nodes on a route (wildcards supported)
11
+ * - `*` - Match any node (wildcard)
12
+ */
13
+
14
+ import type {
15
+ AeonAnyCapability,
16
+ AeonCapabilityActionType,
17
+ AeonNodeCapability,
18
+ AeonNodeCapabilityAction,
19
+ AeonResourceType,
20
+ MerkleAccessRequest,
21
+ ParsedResource,
22
+ } from '../types';
23
+
24
+ // ============================================================================
25
+ // Resource Parsing
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Parse a resource identifier from a capability
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * parseResource('merkle:a1b2c3d4e5f6')
34
+ * // { type: 'merkle', value: 'a1b2c3d4e5f6' }
35
+ *
36
+ * parseResource('tree:a1b2c3d4e5f6')
37
+ * // { type: 'tree', value: 'a1b2c3d4e5f6' }
38
+ *
39
+ * parseResource('path:/dashboard/*')
40
+ * // { type: 'path', value: '/dashboard/*' }
41
+ *
42
+ * parseResource('*')
43
+ * // { type: 'wildcard', value: '*' }
44
+ * ```
45
+ */
46
+ export function parseResource(resource: string): ParsedResource {
47
+ if (resource === '*') {
48
+ return { type: 'wildcard', value: '*' };
49
+ }
50
+
51
+ const colonIndex = resource.indexOf(':');
52
+ if (colonIndex === -1) {
53
+ // No prefix - treat as merkle hash
54
+ return { type: 'merkle', value: resource };
55
+ }
56
+
57
+ const prefix = resource.slice(0, colonIndex);
58
+ const value = resource.slice(colonIndex + 1);
59
+
60
+ switch (prefix) {
61
+ case 'merkle':
62
+ return { type: 'merkle', value };
63
+ case 'tree':
64
+ return { type: 'tree', value };
65
+ case 'path':
66
+ return { type: 'path', value };
67
+ default:
68
+ // Unknown prefix - treat entire string as merkle hash
69
+ return { type: 'merkle', value: resource };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Format a resource identifier for a capability
75
+ */
76
+ export function formatResource(type: AeonResourceType, value: string): string {
77
+ if (type === 'wildcard') return '*';
78
+ return `${type}:${value}`;
79
+ }
80
+
81
+ // ============================================================================
82
+ // Capability Matching
83
+ // ============================================================================
84
+
85
+ /**
86
+ * Check if an action is permitted by a capability action
87
+ */
88
+ function actionPermits(
89
+ capabilityAction: AeonCapabilityActionType,
90
+ requestedAction: 'read' | 'write',
91
+ ): boolean {
92
+ // Wildcard permits everything
93
+ if (capabilityAction === 'aeon:*' || capabilityAction === 'aeon:node:*') {
94
+ return true;
95
+ }
96
+
97
+ // Admin permits everything
98
+ if (capabilityAction === 'aeon:admin') {
99
+ return true;
100
+ }
101
+
102
+ // Check specific actions
103
+ if (requestedAction === 'read') {
104
+ return (
105
+ capabilityAction === 'aeon:read' ||
106
+ capabilityAction === 'aeon:write' ||
107
+ capabilityAction === 'aeon:node:read' ||
108
+ capabilityAction === 'aeon:node:write'
109
+ );
110
+ }
111
+
112
+ if (requestedAction === 'write') {
113
+ return (
114
+ capabilityAction === 'aeon:write' ||
115
+ capabilityAction === 'aeon:node:write'
116
+ );
117
+ }
118
+
119
+ return false;
120
+ }
121
+
122
+ /**
123
+ * Check if a path pattern matches a route
124
+ * Supports wildcards: `/dashboard/*` matches `/dashboard/settings`
125
+ */
126
+ function pathMatches(pattern: string, path: string): boolean {
127
+ // Exact match
128
+ if (pattern === path) return true;
129
+
130
+ // Wildcard pattern
131
+ if (pattern.endsWith('/*')) {
132
+ const prefix = pattern.slice(0, -2);
133
+ return path.startsWith(prefix);
134
+ }
135
+
136
+ // Double wildcard (match any depth)
137
+ if (pattern.endsWith('/**')) {
138
+ const prefix = pattern.slice(0, -3);
139
+ return path.startsWith(prefix);
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ /**
146
+ * Check if a capability grants access to a Merkle node
147
+ */
148
+ export function capabilityGrantsAccess(
149
+ capability: AeonAnyCapability,
150
+ request: MerkleAccessRequest,
151
+ action: 'read' | 'write',
152
+ ): boolean {
153
+ // Check action first
154
+ if (!actionPermits(capability.can, action)) {
155
+ return false;
156
+ }
157
+
158
+ // Parse the resource
159
+ const resource = parseResource(capability.with);
160
+
161
+ switch (resource.type) {
162
+ case 'wildcard':
163
+ // Wildcard grants access to all nodes
164
+ return true;
165
+
166
+ case 'merkle':
167
+ // Exact Merkle hash match
168
+ return resource.value === request.merkleHash;
169
+
170
+ case 'tree':
171
+ // Match if the node or any ancestor has this hash
172
+ if (resource.value === request.merkleHash) {
173
+ return true;
174
+ }
175
+ // Check ancestors
176
+ if (request.ancestorHashes) {
177
+ return request.ancestorHashes.includes(resource.value);
178
+ }
179
+ return false;
180
+
181
+ case 'path':
182
+ // Match based on route path
183
+ if (request.routePath) {
184
+ return pathMatches(resource.value, request.routePath);
185
+ }
186
+ return false;
187
+
188
+ default:
189
+ return false;
190
+ }
191
+ }
192
+
193
+ // ============================================================================
194
+ // Capability Verification
195
+ // ============================================================================
196
+
197
+ /**
198
+ * Node capability verifier function type
199
+ */
200
+ export type NodeCapabilityVerifier = (
201
+ request: MerkleAccessRequest,
202
+ action: 'read' | 'write',
203
+ ) => Promise<boolean>;
204
+
205
+ /**
206
+ * Options for creating a node capability verifier
207
+ */
208
+ export interface NodeVerifierOptions {
209
+ /**
210
+ * Function to extract capabilities from a token
211
+ * Should return the list of capabilities granted by the token
212
+ */
213
+ extractCapabilities: (token: string) => Promise<AeonAnyCapability[]>;
214
+
215
+ /**
216
+ * Function to verify the token is valid (signature, expiry, etc.)
217
+ * If not provided, tokens are assumed valid
218
+ */
219
+ verifyToken?: (token: string) => Promise<boolean>;
220
+
221
+ /**
222
+ * Cache for capability lookups (optional)
223
+ * Key: token, Value: capabilities
224
+ */
225
+ cache?: Map<string, AeonAnyCapability[]>;
226
+
227
+ /**
228
+ * Cache TTL in milliseconds (default: 5 minutes)
229
+ */
230
+ cacheTtlMs?: number;
231
+ }
232
+
233
+ /**
234
+ * Create a node capability verifier from a token
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * const verifier = createNodeCapabilityVerifier(token, {
239
+ * extractCapabilities: async (t) => {
240
+ * const decoded = await decodeUCAN(t);
241
+ * return decoded.capabilities;
242
+ * },
243
+ * verifyToken: async (t) => {
244
+ * return await verifyUCANSignature(t);
245
+ * },
246
+ * });
247
+ *
248
+ * // Check if user can read a specific node
249
+ * const canRead = await verifier(
250
+ * { merkleHash: 'a1b2c3d4e5f6' },
251
+ * 'read'
252
+ * );
253
+ * ```
254
+ */
255
+ export function createNodeCapabilityVerifier(
256
+ token: string,
257
+ options: NodeVerifierOptions,
258
+ ): NodeCapabilityVerifier {
259
+ let cachedCapabilities: AeonAnyCapability[] | null = null;
260
+ let cacheTime = 0;
261
+ const ttl = options.cacheTtlMs ?? 5 * 60 * 1000; // 5 minutes default
262
+
263
+ return async (
264
+ request: MerkleAccessRequest,
265
+ action: 'read' | 'write',
266
+ ): Promise<boolean> => {
267
+ // Verify token if verifier provided
268
+ if (options.verifyToken) {
269
+ const isValid = await options.verifyToken(token);
270
+ if (!isValid) {
271
+ return false;
272
+ }
273
+ }
274
+
275
+ // Get capabilities (with caching)
276
+ const now = Date.now();
277
+ if (!cachedCapabilities || now - cacheTime > ttl) {
278
+ // Check external cache first
279
+ if (options.cache?.has(token)) {
280
+ cachedCapabilities = options.cache.get(token)!;
281
+ } else {
282
+ cachedCapabilities = await options.extractCapabilities(token);
283
+ options.cache?.set(token, cachedCapabilities);
284
+ }
285
+ cacheTime = now;
286
+ }
287
+
288
+ // Check if any capability grants access
289
+ for (const capability of cachedCapabilities) {
290
+ if (capabilityGrantsAccess(capability, request, action)) {
291
+ return true;
292
+ }
293
+ }
294
+
295
+ return false;
296
+ };
297
+ }
298
+
299
+ // ============================================================================
300
+ // Capability Creation Helpers
301
+ // ============================================================================
302
+
303
+ /**
304
+ * Create a node read capability for a specific Merkle hash
305
+ */
306
+ export function createNodeReadCapability(
307
+ merkleHash: string,
308
+ ): AeonNodeCapability {
309
+ return {
310
+ can: 'aeon:node:read',
311
+ with: formatResource('merkle', merkleHash),
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Create a node write capability for a specific Merkle hash
317
+ */
318
+ export function createNodeWriteCapability(
319
+ merkleHash: string,
320
+ ): AeonNodeCapability {
321
+ return {
322
+ can: 'aeon:node:write',
323
+ with: formatResource('merkle', merkleHash),
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Create a tree capability (grants access to node and all descendants)
329
+ */
330
+ export function createTreeCapability(
331
+ merkleHash: string,
332
+ action: AeonNodeCapabilityAction = 'aeon:node:*',
333
+ ): AeonNodeCapability {
334
+ return {
335
+ can: action,
336
+ with: formatResource('tree', merkleHash),
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Create a path-based capability (grants access to all nodes on a route)
342
+ */
343
+ export function createPathCapability(
344
+ routePath: string,
345
+ action: AeonNodeCapabilityAction = 'aeon:node:*',
346
+ ): AeonNodeCapability {
347
+ return {
348
+ can: action,
349
+ with: formatResource('path', routePath),
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Create a wildcard capability (grants access to all nodes)
355
+ */
356
+ export function createWildcardNodeCapability(
357
+ action: AeonNodeCapabilityAction = 'aeon:node:*',
358
+ ): AeonNodeCapability {
359
+ return {
360
+ can: action,
361
+ with: '*',
362
+ };
363
+ }
364
+
365
+ // ============================================================================
366
+ // Access Control Helpers
367
+ // ============================================================================
368
+
369
+ /**
370
+ * Check if a user has access to a node
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * const hasAccess = await checkNodeAccess(
375
+ * capabilities,
376
+ * { merkleHash: 'a1b2c3d4e5f6', routePath: '/dashboard' },
377
+ * 'read'
378
+ * );
379
+ * ```
380
+ */
381
+ export function checkNodeAccess(
382
+ capabilities: AeonAnyCapability[],
383
+ request: MerkleAccessRequest,
384
+ action: 'read' | 'write',
385
+ ): boolean {
386
+ for (const capability of capabilities) {
387
+ if (capabilityGrantsAccess(capability, request, action)) {
388
+ return true;
389
+ }
390
+ }
391
+ return false;
392
+ }
393
+
394
+ /**
395
+ * Filter a tree to only include nodes the user has access to
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * const accessibleNodes = filterAccessibleNodes(
400
+ * nodeMap,
401
+ * capabilities,
402
+ * 'read'
403
+ * );
404
+ * ```
405
+ */
406
+ export function filterAccessibleNodes<
407
+ T extends { merkleHash: string; treePath?: string[] },
408
+ >(
409
+ nodes: T[],
410
+ capabilities: AeonAnyCapability[],
411
+ action: 'read' | 'write',
412
+ routePath?: string,
413
+ ): T[] {
414
+ return nodes.filter((node) => {
415
+ const request: MerkleAccessRequest = {
416
+ merkleHash: node.merkleHash,
417
+ treePath: node.treePath,
418
+ routePath,
419
+ };
420
+ return checkNodeAccess(capabilities, request, action);
421
+ });
422
+ }
423
+
424
+ /**
425
+ * Get the most specific capability for a node
426
+ * Useful for determining the level of access a user has
427
+ */
428
+ export function getMostSpecificCapability(
429
+ capabilities: AeonAnyCapability[],
430
+ request: MerkleAccessRequest,
431
+ ): AeonAnyCapability | null {
432
+ let mostSpecific: AeonAnyCapability | null = null;
433
+ let specificity = -1;
434
+
435
+ for (const capability of capabilities) {
436
+ const resource = parseResource(capability.with);
437
+
438
+ // Calculate specificity (higher = more specific)
439
+ let capSpecificity = 0;
440
+ switch (resource.type) {
441
+ case 'merkle':
442
+ if (resource.value === request.merkleHash) {
443
+ capSpecificity = 4; // Most specific - exact match
444
+ }
445
+ break;
446
+ case 'tree':
447
+ if (resource.value === request.merkleHash) {
448
+ capSpecificity = 3;
449
+ } else if (request.ancestorHashes?.includes(resource.value)) {
450
+ capSpecificity = 2;
451
+ }
452
+ break;
453
+ case 'path':
454
+ if (
455
+ request.routePath &&
456
+ pathMatches(resource.value, request.routePath)
457
+ ) {
458
+ capSpecificity = 1;
459
+ }
460
+ break;
461
+ case 'wildcard':
462
+ capSpecificity = 0; // Least specific
463
+ break;
464
+ }
465
+
466
+ if (capSpecificity > specificity) {
467
+ specificity = capSpecificity;
468
+ mostSpecific = capability;
469
+ }
470
+ }
471
+
472
+ return mostSpecific;
473
+ }