@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,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
|
+
}
|