@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,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @affectively/aeon-pages-analytics
|
|
3
|
+
*
|
|
4
|
+
* Automatic click tracking and GTM integration for aeon-pages.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Merkle tree-based node identification
|
|
8
|
+
* - Zero-instrumentation click tracking
|
|
9
|
+
* - ESI context → GTM dataLayer sync
|
|
10
|
+
* - Full tree path with every click event
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { AeonAnalyticsProvider } from '@affectively/aeon-pages-analytics/react';
|
|
15
|
+
*
|
|
16
|
+
* export default function App({ Component, pageProps }) {
|
|
17
|
+
* return (
|
|
18
|
+
* <AeonAnalyticsProvider
|
|
19
|
+
* gtmContainerId="GTM-XXXXXX"
|
|
20
|
+
* trackClicks={true}
|
|
21
|
+
* syncESIContext={true}
|
|
22
|
+
* >
|
|
23
|
+
* <Component {...pageProps} />
|
|
24
|
+
* </AeonAnalyticsProvider>
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Types
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
export type {
|
|
35
|
+
// Configuration
|
|
36
|
+
GTMConfig,
|
|
37
|
+
AnalyticsConfig,
|
|
38
|
+
ClickTrackingOptions,
|
|
39
|
+
|
|
40
|
+
// Merkle Tree
|
|
41
|
+
MerkleNode,
|
|
42
|
+
MerkleTree,
|
|
43
|
+
ComponentNode,
|
|
44
|
+
ComponentTree,
|
|
45
|
+
SerializedMerkleInfo,
|
|
46
|
+
|
|
47
|
+
// ESI State
|
|
48
|
+
UserTier,
|
|
49
|
+
ConnectionType,
|
|
50
|
+
EmotionState,
|
|
51
|
+
ESIState,
|
|
52
|
+
ESIStateFeatures,
|
|
53
|
+
|
|
54
|
+
// DataLayer Events
|
|
55
|
+
AeonEventBase,
|
|
56
|
+
ElementInfo,
|
|
57
|
+
PositionInfo,
|
|
58
|
+
ContextEvent,
|
|
59
|
+
PageViewEvent,
|
|
60
|
+
ClickEvent,
|
|
61
|
+
DataLayerEvent,
|
|
62
|
+
} from './types';
|
|
63
|
+
|
|
64
|
+
// Constants
|
|
65
|
+
export { MERKLE_ATTR, PATH_ATTR, PATH_HASHES_ATTR, TYPE_ATTR } from './types';
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Merkle Tree
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
// Async builders
|
|
73
|
+
hashNodeAsync,
|
|
74
|
+
buildMerkleTree,
|
|
75
|
+
|
|
76
|
+
// Sync builders
|
|
77
|
+
hashNodeSync,
|
|
78
|
+
buildMerkleTreeSync,
|
|
79
|
+
|
|
80
|
+
// DOM helpers
|
|
81
|
+
getMerkleAttributes,
|
|
82
|
+
parseMerkleFromElement,
|
|
83
|
+
findNearestMerkleElement,
|
|
84
|
+
|
|
85
|
+
// Verification
|
|
86
|
+
verifyMerkleTree,
|
|
87
|
+
diffMerkleTrees,
|
|
88
|
+
} from './merkle-tree';
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// DataLayer
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
// Version
|
|
96
|
+
ANALYTICS_VERSION,
|
|
97
|
+
|
|
98
|
+
// DataLayer management
|
|
99
|
+
ensureDataLayer,
|
|
100
|
+
pushToDataLayer,
|
|
101
|
+
getDataLayer,
|
|
102
|
+
clearDataLayer,
|
|
103
|
+
|
|
104
|
+
// Event builders
|
|
105
|
+
buildContextEvent,
|
|
106
|
+
buildPageViewEvent,
|
|
107
|
+
buildClickEvent,
|
|
108
|
+
|
|
109
|
+
// Element/position extraction
|
|
110
|
+
extractElementInfo,
|
|
111
|
+
extractPositionInfo,
|
|
112
|
+
|
|
113
|
+
// Push helpers
|
|
114
|
+
pushContextEvent,
|
|
115
|
+
pushPageViewEvent,
|
|
116
|
+
pushClickEvent,
|
|
117
|
+
|
|
118
|
+
// Debug
|
|
119
|
+
setDebugMode,
|
|
120
|
+
} from './data-layer';
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// GTM Loader
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
// Injection
|
|
128
|
+
injectGTM,
|
|
129
|
+
injectGTMNoScript,
|
|
130
|
+
initializeGTM,
|
|
131
|
+
|
|
132
|
+
// SSR helpers
|
|
133
|
+
generateGTMScriptTag,
|
|
134
|
+
generateGTMNoScriptTag,
|
|
135
|
+
generateDataLayerScript,
|
|
136
|
+
|
|
137
|
+
// Status
|
|
138
|
+
isGTMInjected,
|
|
139
|
+
isGTMReady,
|
|
140
|
+
waitForGTM,
|
|
141
|
+
|
|
142
|
+
// Testing
|
|
143
|
+
resetGTMState,
|
|
144
|
+
} from './gtm-loader';
|
|
145
|
+
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// Context Bridge
|
|
148
|
+
// ============================================================================
|
|
149
|
+
|
|
150
|
+
export {
|
|
151
|
+
// ESI state access
|
|
152
|
+
getESIState,
|
|
153
|
+
hasESIState,
|
|
154
|
+
getESIProperty,
|
|
155
|
+
|
|
156
|
+
// Subscription
|
|
157
|
+
subscribeToESIChanges,
|
|
158
|
+
|
|
159
|
+
// DataLayer sync
|
|
160
|
+
syncESIToDataLayer,
|
|
161
|
+
pushPageView,
|
|
162
|
+
|
|
163
|
+
// Watch mode
|
|
164
|
+
watchESIChanges,
|
|
165
|
+
initContextBridge,
|
|
166
|
+
|
|
167
|
+
// Retry logic
|
|
168
|
+
waitForESIState,
|
|
169
|
+
initContextBridgeWithRetry,
|
|
170
|
+
|
|
171
|
+
// Utilities
|
|
172
|
+
getESIContextSnapshot,
|
|
173
|
+
isAdmin,
|
|
174
|
+
hasFeature,
|
|
175
|
+
meetsTierRequirement,
|
|
176
|
+
getUserTier,
|
|
177
|
+
getEmotionState,
|
|
178
|
+
} from './context-bridge';
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Click Tracker
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
// Initialization
|
|
186
|
+
initClickTracker,
|
|
187
|
+
stopClickTracker,
|
|
188
|
+
isClickTrackerActive,
|
|
189
|
+
|
|
190
|
+
// Manual tracking
|
|
191
|
+
trackClick,
|
|
192
|
+
trackInteraction,
|
|
193
|
+
} from './click-tracker';
|
|
194
|
+
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// React
|
|
197
|
+
// ============================================================================
|
|
198
|
+
|
|
199
|
+
export {
|
|
200
|
+
// Main hook
|
|
201
|
+
useAeonAnalytics,
|
|
202
|
+
type UseAnalyticsReturn,
|
|
203
|
+
|
|
204
|
+
// Utility hooks
|
|
205
|
+
useESIState,
|
|
206
|
+
useTrackMount,
|
|
207
|
+
useTrackVisibility,
|
|
208
|
+
} from './use-analytics';
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
// Provider
|
|
212
|
+
AeonAnalyticsProvider,
|
|
213
|
+
type AeonAnalyticsProviderProps,
|
|
214
|
+
|
|
215
|
+
// Context hooks
|
|
216
|
+
useAnalytics,
|
|
217
|
+
useAnalyticsOptional,
|
|
218
|
+
|
|
219
|
+
// HOC
|
|
220
|
+
withAnalytics,
|
|
221
|
+
|
|
222
|
+
// Render props
|
|
223
|
+
Analytics,
|
|
224
|
+
|
|
225
|
+
// Merkle tree provider
|
|
226
|
+
MerkleTreeProvider,
|
|
227
|
+
|
|
228
|
+
// Track component
|
|
229
|
+
Track,
|
|
230
|
+
} from './provider';
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merkle Tree Node Identification
|
|
3
|
+
*
|
|
4
|
+
* Generates deterministic, content-addressable hashes for component tree nodes.
|
|
5
|
+
* Uses SHA-256 with children hashes to create a Merkle tree structure.
|
|
6
|
+
*
|
|
7
|
+
* Benefits:
|
|
8
|
+
* - Stable IDs across renders (if content unchanged)
|
|
9
|
+
* - Content-addressable (same content = same hash)
|
|
10
|
+
* - Tree-aware (parent hash changes if any child changes)
|
|
11
|
+
* - Position-independent (moves don't change hash)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ComponentNode,
|
|
16
|
+
ComponentTree,
|
|
17
|
+
MerkleNode,
|
|
18
|
+
MerkleTree,
|
|
19
|
+
MERKLE_ATTR,
|
|
20
|
+
PATH_ATTR,
|
|
21
|
+
PATH_HASHES_ATTR,
|
|
22
|
+
TYPE_ATTR,
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Crypto Utilities
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* SHA-256 hash with Web Crypto API (works in browser and Bun/Node)
|
|
31
|
+
*/
|
|
32
|
+
async function sha256(message: string): Promise<string> {
|
|
33
|
+
const encoder = new TextEncoder();
|
|
34
|
+
const data = encoder.encode(message);
|
|
35
|
+
|
|
36
|
+
// Use Web Crypto API
|
|
37
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
38
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
39
|
+
const hashHex = hashArray
|
|
40
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
41
|
+
.join('');
|
|
42
|
+
|
|
43
|
+
return hashHex;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Synchronous hash using simple djb2 algorithm for performance
|
|
48
|
+
* Used for quick hashing when async is not desired
|
|
49
|
+
*/
|
|
50
|
+
function djb2Hash(str: string): string {
|
|
51
|
+
let hash = 5381;
|
|
52
|
+
for (let i = 0; i < str.length; i++) {
|
|
53
|
+
hash = (hash * 33) ^ str.charCodeAt(i);
|
|
54
|
+
}
|
|
55
|
+
// Convert to positive hex, pad to 12 chars
|
|
56
|
+
return ((hash >>> 0).toString(16) + (hash >>> 0).toString(16)).slice(0, 12);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Truncate hash to 12 characters for compact storage
|
|
61
|
+
*/
|
|
62
|
+
function truncateHash(hash: string): string {
|
|
63
|
+
return hash.slice(0, 12);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Key Sorting for Deterministic JSON
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Sort object keys recursively for deterministic serialization
|
|
72
|
+
*/
|
|
73
|
+
function sortKeys(obj: unknown): unknown {
|
|
74
|
+
if (obj === null || typeof obj !== 'object') {
|
|
75
|
+
return obj;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(obj)) {
|
|
79
|
+
return obj.map(sortKeys);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sorted: Record<string, unknown> = {};
|
|
83
|
+
const keys = Object.keys(obj as Record<string, unknown>).sort();
|
|
84
|
+
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
sorted[key] = sortKeys((obj as Record<string, unknown>)[key]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return sorted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sanitize props for hashing - remove functions, symbols, etc.
|
|
94
|
+
*/
|
|
95
|
+
function sanitizeProps(
|
|
96
|
+
props: Record<string, unknown> | undefined,
|
|
97
|
+
): Record<string, unknown> {
|
|
98
|
+
if (!props) return {};
|
|
99
|
+
|
|
100
|
+
const sanitized: Record<string, unknown> = {};
|
|
101
|
+
|
|
102
|
+
for (const [key, value] of Object.entries(props)) {
|
|
103
|
+
// Skip functions, symbols, and React internals
|
|
104
|
+
if (
|
|
105
|
+
typeof value === 'function' ||
|
|
106
|
+
typeof value === 'symbol' ||
|
|
107
|
+
key.startsWith('_') ||
|
|
108
|
+
key === 'children' ||
|
|
109
|
+
key === 'ref' ||
|
|
110
|
+
key === 'key'
|
|
111
|
+
) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Recursively sanitize nested objects
|
|
116
|
+
if (value !== null && typeof value === 'object') {
|
|
117
|
+
sanitized[key] = sanitizeProps(value as Record<string, unknown>);
|
|
118
|
+
} else {
|
|
119
|
+
sanitized[key] = value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return sanitized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Hash Generation
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate deterministic hash for a single node
|
|
132
|
+
* Hash = SHA-256(type + sortedProps + childHashes)
|
|
133
|
+
*/
|
|
134
|
+
export async function hashNodeAsync(
|
|
135
|
+
type: string,
|
|
136
|
+
props: Record<string, unknown>,
|
|
137
|
+
childHashes: string[],
|
|
138
|
+
): Promise<string> {
|
|
139
|
+
const content = JSON.stringify({
|
|
140
|
+
type,
|
|
141
|
+
props: sortKeys(sanitizeProps(props)),
|
|
142
|
+
children: childHashes.sort(), // Sort for consistency
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const fullHash = await sha256(content);
|
|
146
|
+
return truncateHash(fullHash);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Synchronous hash generation using djb2
|
|
151
|
+
* Faster but less collision-resistant
|
|
152
|
+
*/
|
|
153
|
+
export function hashNodeSync(
|
|
154
|
+
type: string,
|
|
155
|
+
props: Record<string, unknown>,
|
|
156
|
+
childHashes: string[],
|
|
157
|
+
): string {
|
|
158
|
+
const content = JSON.stringify({
|
|
159
|
+
type,
|
|
160
|
+
props: sortKeys(sanitizeProps(props)),
|
|
161
|
+
children: childHashes.sort(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return djb2Hash(content);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Merkle Tree Builder
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
interface BuildContext {
|
|
172
|
+
nodes: Map<string, MerkleNode>;
|
|
173
|
+
hashToId: Map<string, string>;
|
|
174
|
+
tree: ComponentTree;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Recursively build MerkleNode for a node and its children
|
|
179
|
+
* Uses post-order traversal (children first, then parent)
|
|
180
|
+
*/
|
|
181
|
+
async function buildNodeAsync(
|
|
182
|
+
nodeId: string,
|
|
183
|
+
parentPath: string[],
|
|
184
|
+
parentPathHashes: string[],
|
|
185
|
+
depth: number,
|
|
186
|
+
ctx: BuildContext,
|
|
187
|
+
): Promise<MerkleNode | null> {
|
|
188
|
+
const node = ctx.tree.getNode(nodeId);
|
|
189
|
+
if (!node) return null;
|
|
190
|
+
|
|
191
|
+
// Process children first (post-order)
|
|
192
|
+
const childHashes: string[] = [];
|
|
193
|
+
const children = ctx.tree.getChildren(nodeId);
|
|
194
|
+
|
|
195
|
+
for (const child of children) {
|
|
196
|
+
const childNode = await buildNodeAsync(
|
|
197
|
+
child.id,
|
|
198
|
+
[...parentPath, node.type],
|
|
199
|
+
[...parentPathHashes], // Will be filled after we compute our hash
|
|
200
|
+
depth + 1,
|
|
201
|
+
ctx,
|
|
202
|
+
);
|
|
203
|
+
if (childNode) {
|
|
204
|
+
childHashes.push(childNode.hash);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Now hash this node
|
|
209
|
+
const hash = await hashNodeAsync(node.type, node.props || {}, childHashes);
|
|
210
|
+
|
|
211
|
+
// Build path with hashes
|
|
212
|
+
const path = [...parentPath, node.type];
|
|
213
|
+
const pathHashes = [...parentPathHashes, hash];
|
|
214
|
+
|
|
215
|
+
const merkleNode: MerkleNode = {
|
|
216
|
+
hash,
|
|
217
|
+
originalId: nodeId,
|
|
218
|
+
type: node.type,
|
|
219
|
+
props: sanitizeProps(node.props),
|
|
220
|
+
childHashes,
|
|
221
|
+
path,
|
|
222
|
+
pathHashes,
|
|
223
|
+
depth,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
ctx.nodes.set(nodeId, merkleNode);
|
|
227
|
+
ctx.hashToId.set(hash, nodeId);
|
|
228
|
+
|
|
229
|
+
// Update children with correct parent path hashes
|
|
230
|
+
for (const child of children) {
|
|
231
|
+
const childMerkle = ctx.nodes.get(child.id);
|
|
232
|
+
if (childMerkle) {
|
|
233
|
+
childMerkle.pathHashes = [...pathHashes, childMerkle.hash];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return merkleNode;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Synchronous version for server-side rendering
|
|
242
|
+
*/
|
|
243
|
+
function buildNodeSync(
|
|
244
|
+
nodeId: string,
|
|
245
|
+
parentPath: string[],
|
|
246
|
+
parentPathHashes: string[],
|
|
247
|
+
depth: number,
|
|
248
|
+
ctx: BuildContext,
|
|
249
|
+
): MerkleNode | null {
|
|
250
|
+
const node = ctx.tree.getNode(nodeId);
|
|
251
|
+
if (!node) return null;
|
|
252
|
+
|
|
253
|
+
// Process children first (post-order)
|
|
254
|
+
const childHashes: string[] = [];
|
|
255
|
+
const children = ctx.tree.getChildren(nodeId);
|
|
256
|
+
|
|
257
|
+
for (const child of children) {
|
|
258
|
+
const childNode = buildNodeSync(
|
|
259
|
+
child.id,
|
|
260
|
+
[...parentPath, node.type],
|
|
261
|
+
[...parentPathHashes],
|
|
262
|
+
depth + 1,
|
|
263
|
+
ctx,
|
|
264
|
+
);
|
|
265
|
+
if (childNode) {
|
|
266
|
+
childHashes.push(childNode.hash);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Hash this node
|
|
271
|
+
const hash = hashNodeSync(node.type, node.props || {}, childHashes);
|
|
272
|
+
|
|
273
|
+
// Build path with hashes
|
|
274
|
+
const path = [...parentPath, node.type];
|
|
275
|
+
const pathHashes = [...parentPathHashes, hash];
|
|
276
|
+
|
|
277
|
+
const merkleNode: MerkleNode = {
|
|
278
|
+
hash,
|
|
279
|
+
originalId: nodeId,
|
|
280
|
+
type: node.type,
|
|
281
|
+
props: sanitizeProps(node.props),
|
|
282
|
+
childHashes,
|
|
283
|
+
path,
|
|
284
|
+
pathHashes,
|
|
285
|
+
depth,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
ctx.nodes.set(nodeId, merkleNode);
|
|
289
|
+
ctx.hashToId.set(hash, nodeId);
|
|
290
|
+
|
|
291
|
+
// Update children with correct parent path hashes
|
|
292
|
+
for (const child of children) {
|
|
293
|
+
const childMerkle = ctx.nodes.get(child.id);
|
|
294
|
+
if (childMerkle) {
|
|
295
|
+
childMerkle.pathHashes = [...pathHashes, childMerkle.hash];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return merkleNode;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Build complete Merkle tree from ComponentTree (async)
|
|
304
|
+
*/
|
|
305
|
+
export async function buildMerkleTree(
|
|
306
|
+
tree: ComponentTree,
|
|
307
|
+
): Promise<MerkleTree> {
|
|
308
|
+
const ctx: BuildContext = {
|
|
309
|
+
nodes: new Map(),
|
|
310
|
+
hashToId: new Map(),
|
|
311
|
+
tree,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const rootNode = await buildNodeAsync(tree.rootId, [], [], 0, ctx);
|
|
315
|
+
|
|
316
|
+
const merkleTree: MerkleTree = {
|
|
317
|
+
rootHash: rootNode?.hash || '',
|
|
318
|
+
nodes: ctx.nodes,
|
|
319
|
+
|
|
320
|
+
getNode(id: string): MerkleNode | undefined {
|
|
321
|
+
return ctx.nodes.get(id);
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
getNodeByHash(hash: string): MerkleNode | undefined {
|
|
325
|
+
const id = ctx.hashToId.get(hash);
|
|
326
|
+
return id ? ctx.nodes.get(id) : undefined;
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
getNodesAtDepth(depth: number): MerkleNode[] {
|
|
330
|
+
return Array.from(ctx.nodes.values()).filter((n) => n.depth === depth);
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
return merkleTree;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Build complete Merkle tree from ComponentTree (sync)
|
|
339
|
+
*/
|
|
340
|
+
export function buildMerkleTreeSync(tree: ComponentTree): MerkleTree {
|
|
341
|
+
const ctx: BuildContext = {
|
|
342
|
+
nodes: new Map(),
|
|
343
|
+
hashToId: new Map(),
|
|
344
|
+
tree,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const rootNode = buildNodeSync(tree.rootId, [], [], 0, ctx);
|
|
348
|
+
|
|
349
|
+
const merkleTree: MerkleTree = {
|
|
350
|
+
rootHash: rootNode?.hash || '',
|
|
351
|
+
nodes: ctx.nodes,
|
|
352
|
+
|
|
353
|
+
getNode(id: string): MerkleNode | undefined {
|
|
354
|
+
return ctx.nodes.get(id);
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
getNodeByHash(hash: string): MerkleNode | undefined {
|
|
358
|
+
const id = ctx.hashToId.get(hash);
|
|
359
|
+
return id ? ctx.nodes.get(id) : undefined;
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
getNodesAtDepth(depth: number): MerkleNode[] {
|
|
363
|
+
return Array.from(ctx.nodes.values()).filter((n) => n.depth === depth);
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return merkleTree;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================================
|
|
371
|
+
// DOM Attribute Helpers
|
|
372
|
+
// ============================================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Generate data attributes for a DOM element
|
|
376
|
+
*/
|
|
377
|
+
export function getMerkleAttributes(
|
|
378
|
+
merkleNode: MerkleNode,
|
|
379
|
+
): Record<string, string> {
|
|
380
|
+
return {
|
|
381
|
+
'data-aeon-merkle': merkleNode.hash,
|
|
382
|
+
'data-aeon-path': JSON.stringify(merkleNode.path),
|
|
383
|
+
'data-aeon-path-hashes': JSON.stringify(merkleNode.pathHashes),
|
|
384
|
+
'data-aeon-type': merkleNode.type,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Parse Merkle info from DOM element
|
|
390
|
+
*/
|
|
391
|
+
export function parseMerkleFromElement(element: HTMLElement): {
|
|
392
|
+
hash: string;
|
|
393
|
+
path: string[];
|
|
394
|
+
pathHashes: string[];
|
|
395
|
+
type: string;
|
|
396
|
+
} | null {
|
|
397
|
+
const hash = element.getAttribute('data-aeon-merkle');
|
|
398
|
+
if (!hash) return null;
|
|
399
|
+
|
|
400
|
+
const pathStr = element.getAttribute('data-aeon-path');
|
|
401
|
+
const pathHashesStr = element.getAttribute('data-aeon-path-hashes');
|
|
402
|
+
const type = element.getAttribute('data-aeon-type') || 'unknown';
|
|
403
|
+
|
|
404
|
+
let path: string[] = [];
|
|
405
|
+
let pathHashes: string[] = [];
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
if (pathStr) path = JSON.parse(pathStr);
|
|
409
|
+
if (pathHashesStr) pathHashes = JSON.parse(pathHashesStr);
|
|
410
|
+
} catch {
|
|
411
|
+
// Invalid JSON, use empty arrays
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return { hash, path, pathHashes, type };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Find nearest ancestor with Merkle attributes
|
|
419
|
+
*/
|
|
420
|
+
export function findNearestMerkleElement(
|
|
421
|
+
element: HTMLElement,
|
|
422
|
+
): HTMLElement | null {
|
|
423
|
+
let current: HTMLElement | null = element;
|
|
424
|
+
|
|
425
|
+
while (current) {
|
|
426
|
+
if (current.hasAttribute('data-aeon-merkle')) {
|
|
427
|
+
return current;
|
|
428
|
+
}
|
|
429
|
+
current = current.parentElement;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ============================================================================
|
|
436
|
+
// Verification Utilities
|
|
437
|
+
// ============================================================================
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Verify tree integrity by recomputing root hash
|
|
441
|
+
*/
|
|
442
|
+
export async function verifyMerkleTree(
|
|
443
|
+
tree: MerkleTree,
|
|
444
|
+
componentTree: ComponentTree,
|
|
445
|
+
): Promise<boolean> {
|
|
446
|
+
const rebuilt = await buildMerkleTree(componentTree);
|
|
447
|
+
return rebuilt.rootHash === tree.rootHash;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Find nodes that have changed between two Merkle trees
|
|
452
|
+
*/
|
|
453
|
+
export function diffMerkleTrees(
|
|
454
|
+
oldTree: MerkleTree,
|
|
455
|
+
newTree: MerkleTree,
|
|
456
|
+
): { added: MerkleNode[]; removed: MerkleNode[]; changed: MerkleNode[] } {
|
|
457
|
+
const added: MerkleNode[] = [];
|
|
458
|
+
const removed: MerkleNode[] = [];
|
|
459
|
+
const changed: MerkleNode[] = [];
|
|
460
|
+
|
|
461
|
+
const oldHashes = new Set(
|
|
462
|
+
Array.from(oldTree.nodes.values()).map((n) => n.hash),
|
|
463
|
+
);
|
|
464
|
+
const newHashes = new Set(
|
|
465
|
+
Array.from(newTree.nodes.values()).map((n) => n.hash),
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// Find added nodes (in new but not in old)
|
|
469
|
+
for (const [, node] of newTree.nodes) {
|
|
470
|
+
if (!oldHashes.has(node.hash)) {
|
|
471
|
+
// Check if originalId exists in old tree (changed) or not (added)
|
|
472
|
+
const oldNode = oldTree.getNode(node.originalId);
|
|
473
|
+
if (oldNode) {
|
|
474
|
+
changed.push(node);
|
|
475
|
+
} else {
|
|
476
|
+
added.push(node);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Find removed nodes (in old but not in new by ID)
|
|
482
|
+
for (const [id, node] of oldTree.nodes) {
|
|
483
|
+
if (!newTree.getNode(id)) {
|
|
484
|
+
removed.push(node);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return { added, removed, changed };
|
|
489
|
+
}
|