@adukiorg/anza 0.2.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 +137 -0
- package/README.md +215 -0
- package/bin/anza.js +63 -0
- package/bin/create.js +150 -0
- package/importmap.json +72 -0
- package/package.json +100 -0
- package/src/core/animations/index.js +55 -0
- package/src/core/animations/play.js +111 -0
- package/src/core/animations/registry.js +54 -0
- package/src/core/animations/scroll.js +50 -0
- package/src/core/animations/tokens.js +58 -0
- package/src/core/animations/usage.md +301 -0
- package/src/core/animations/waapi.js +86 -0
- package/src/core/api/cache.js +120 -0
- package/src/core/api/caches/glob.js +24 -0
- package/src/core/api/caches/index.js +118 -0
- package/src/core/api/events/index.js +75 -0
- package/src/core/api/fetch.js +99 -0
- package/src/core/api/index.js +158 -0
- package/src/core/api/pipeline.js +98 -0
- package/src/core/api/plan.md +209 -0
- package/src/core/api/prefixes/index.js +66 -0
- package/src/core/api/retry.js +69 -0
- package/src/core/api/stream.js +127 -0
- package/src/core/api/upload.js +180 -0
- package/src/core/api/usage.md +206 -0
- package/src/core/events/bus.js +38 -0
- package/src/core/events/delegate.js +79 -0
- package/src/core/events/index.js +26 -0
- package/src/core/events/listen.js +50 -0
- package/src/core/events/missing.md +103 -0
- package/src/core/events/once.js +49 -0
- package/src/core/events/plan.md +177 -0
- package/src/core/events/types/index.js +34 -0
- package/src/core/events/usage.md +107 -0
- package/src/core/offline/bridge.js +51 -0
- package/src/core/offline/clock.js +100 -0
- package/src/core/offline/connectivity.js +116 -0
- package/src/core/offline/index.js +41 -0
- package/src/core/offline/missing.md +89 -0
- package/src/core/offline/plan.md +143 -0
- package/src/core/offline/queue.js +168 -0
- package/src/core/offline/state.js +18 -0
- package/src/core/offline/sync.js +106 -0
- package/src/core/offline/usage.md +273 -0
- package/src/core/platform/guard.js +104 -0
- package/src/core/platform/index.js +42 -0
- package/src/core/platform/missing.md +119 -0
- package/src/core/platform/platform.d.ts +88 -0
- package/src/core/platform/polyfills/anchor.js +79 -0
- package/src/core/platform/polyfills/navigation.js +142 -0
- package/src/core/platform/polyfills/popover.js +142 -0
- package/src/core/platform/polyfills/scheduler.js +194 -0
- package/src/core/platform/polyfills/shadow.js +35 -0
- package/src/core/platform/polyfills/urlpattern.js +119 -0
- package/src/core/platform/supports.js +186 -0
- package/src/core/platform/usage.md +287 -0
- package/src/core/router/cache.js +95 -0
- package/src/core/router/container.js +146 -0
- package/src/core/router/handler.js +52 -0
- package/src/core/router/history.js +120 -0
- package/src/core/router/index.js +158 -0
- package/src/core/router/intercept.js +376 -0
- package/src/core/router/match.js +145 -0
- package/src/core/router/missing.md +716 -0
- package/src/core/router/outlet.js +139 -0
- package/src/core/router/plan.md +370 -0
- package/src/core/router/sync/index.js +16 -0
- package/src/core/router/sync/tab.js +115 -0
- package/src/core/router/sync/transport.js +139 -0
- package/src/core/router/transitions.js +59 -0
- package/src/core/router/usage.md +773 -0
- package/src/core/security/crypto.js +159 -0
- package/src/core/security/index.js +49 -0
- package/src/core/security/missing.md +97 -0
- package/src/core/security/permissions.js +64 -0
- package/src/core/security/sanitize.js +100 -0
- package/src/core/security/usage.md +283 -0
- package/src/core/state/derived.js +117 -0
- package/src/core/state/index.js +23 -0
- package/src/core/state/missing.md +165 -0
- package/src/core/state/persist.js +284 -0
- package/src/core/state/store.js +308 -0
- package/src/core/state/sync.js +46 -0
- package/src/core/state/usage.md +440 -0
- package/src/core/storage/cache.js +83 -0
- package/src/core/storage/idb.js +196 -0
- package/src/core/storage/index.js +373 -0
- package/src/core/storage/lru.js +107 -0
- package/src/core/storage/missing.md +165 -0
- package/src/core/storage/opfs.js +190 -0
- package/src/core/storage/plan.md +69 -0
- package/src/core/storage/quota.js +69 -0
- package/src/core/storage/usage.md +226 -0
- package/src/core/ui/base.js +50 -0
- package/src/core/ui/define/container.js +82 -0
- package/src/core/ui/define/define.js +12 -0
- package/src/core/ui/define/element.js +390 -0
- package/src/core/ui/define/index.js +9 -0
- package/src/core/ui/define/orchestrator.js +105 -0
- package/src/core/ui/define/proxy.js +644 -0
- package/src/core/ui/define/state.js +6 -0
- package/src/core/ui/define/utils.js +134 -0
- package/src/core/ui/implementation.md +170 -0
- package/src/core/ui/index.js +41 -0
- package/src/core/ui/observe.js +117 -0
- package/src/core/ui/plan.md +510 -0
- package/src/core/ui/schedule.js +60 -0
- package/src/core/ui/template.js +37 -0
- package/src/core/ui/transitions.js +37 -0
- package/src/core/ui/ui.types.md +890 -0
- package/src/core/ui/usage.md +1124 -0
- package/src/core/ui/watch.md +346 -0
- package/src/core/workers/broadcast.js +138 -0
- package/src/core/workers/dedicated.js +153 -0
- package/src/core/workers/index.js +169 -0
- package/src/core/workers/locks.js +160 -0
- package/src/core/workers/offscreen.js +166 -0
- package/src/core/workers/plan.md +381 -0
- package/src/core/workers/pool.js +267 -0
- package/src/core/workers/shared.js +137 -0
- package/src/core/workers/usage.md +622 -0
- package/src/elements/base.js +12 -0
- package/src/elements/data/card/index.html +9 -0
- package/src/elements/data/card/index.js +19 -0
- package/src/elements/data/card/index.tags.json +1 -0
- package/src/elements/data/card/style.css +46 -0
- package/src/elements/data/chart/index.html +1 -0
- package/src/elements/data/chart/index.js +143 -0
- package/src/elements/data/chart/index.tags.json +1 -0
- package/src/elements/data/chart/style.css +13 -0
- package/src/elements/data/list/index.html +3 -0
- package/src/elements/data/list/index.js +19 -0
- package/src/elements/data/list/index.tags.json +1 -0
- package/src/elements/data/list/style.css +39 -0
- package/src/elements/data/stat/index.html +9 -0
- package/src/elements/data/stat/index.js +19 -0
- package/src/elements/data/stat/index.tags.json +1 -0
- package/src/elements/data/stat/style.css +50 -0
- package/src/elements/data/table/index.html +1 -0
- package/src/elements/data/table/index.js +16 -0
- package/src/elements/data/table/index.tags.json +1 -0
- package/src/elements/data/table/style.css +50 -0
- package/src/elements/feedback/alert/index.html +11 -0
- package/src/elements/feedback/alert/index.js +28 -0
- package/src/elements/feedback/alert/index.tags.json +1 -0
- package/src/elements/feedback/alert/style.css +75 -0
- package/src/elements/feedback/empty/index.html +13 -0
- package/src/elements/feedback/empty/index.js +34 -0
- package/src/elements/feedback/empty/index.tags.json +1 -0
- package/src/elements/feedback/empty/style.css +45 -0
- package/src/elements/feedback/progress/index.html +7 -0
- package/src/elements/feedback/progress/index.js +46 -0
- package/src/elements/feedback/progress/index.tags.json +1 -0
- package/src/elements/feedback/progress/style.css +36 -0
- package/src/elements/feedback/skeleton/index.html +1 -0
- package/src/elements/feedback/skeleton/index.js +78 -0
- package/src/elements/feedback/skeleton/index.tags.json +1 -0
- package/src/elements/feedback/skeleton/style.css +28 -0
- package/src/elements/feedback/toast/index.html +3 -0
- package/src/elements/feedback/toast/index.js +65 -0
- package/src/elements/feedback/toast/index.tags.json +1 -0
- package/src/elements/feedback/toast/style.css +36 -0
- package/src/elements/forms/checkbox/index.html +7 -0
- package/src/elements/forms/checkbox/index.js +104 -0
- package/src/elements/forms/checkbox/index.tags.json +1 -0
- package/src/elements/forms/checkbox/style.css +86 -0
- package/src/elements/forms/field/index.html +13 -0
- package/src/elements/forms/field/index.js +42 -0
- package/src/elements/forms/field/index.tags.json +1 -0
- package/src/elements/forms/field/style.css +42 -0
- package/src/elements/forms/form/index.html +3 -0
- package/src/elements/forms/form/index.js +122 -0
- package/src/elements/forms/form/index.tags.json +1 -0
- package/src/elements/forms/form/style.css +11 -0
- package/src/elements/forms/input/index.html +4 -0
- package/src/elements/forms/input/index.js +103 -0
- package/src/elements/forms/input/index.tags.json +1 -0
- package/src/elements/forms/input/style.css +39 -0
- package/src/elements/forms/radio/index.html +4 -0
- package/src/elements/forms/radio/index.js +109 -0
- package/src/elements/forms/radio/index.tags.json +1 -0
- package/src/elements/forms/radio/style.css +65 -0
- package/src/elements/forms/select/index.html +9 -0
- package/src/elements/forms/select/index.js +114 -0
- package/src/elements/forms/select/index.tags.json +1 -0
- package/src/elements/forms/select/style.css +95 -0
- package/src/elements/forms/textarea/index.html +4 -0
- package/src/elements/forms/textarea/index.js +115 -0
- package/src/elements/forms/textarea/index.tags.json +1 -0
- package/src/elements/forms/textarea/style.css +46 -0
- package/src/elements/forms/toggle/index.html +4 -0
- package/src/elements/forms/toggle/index.js +89 -0
- package/src/elements/forms/toggle/index.tags.json +1 -0
- package/src/elements/forms/toggle/style.css +63 -0
- package/src/elements/forms/upload/index.html +13 -0
- package/src/elements/forms/upload/index.js +120 -0
- package/src/elements/forms/upload/index.tags.json +1 -0
- package/src/elements/forms/upload/style.css +61 -0
- package/src/elements/index.js +71 -0
- package/src/elements/layout/app/index.html +7 -0
- package/src/elements/layout/app/index.js +16 -0
- package/src/elements/layout/app/index.tags.json +1 -0
- package/src/elements/layout/app/style.css +41 -0
- package/src/elements/layout/grid/index.html +3 -0
- package/src/elements/layout/grid/index.js +41 -0
- package/src/elements/layout/grid/index.tags.json +1 -0
- package/src/elements/layout/grid/style.css +12 -0
- package/src/elements/layout/header/index.html +8 -0
- package/src/elements/layout/header/index.js +16 -0
- package/src/elements/layout/header/index.tags.json +1 -0
- package/src/elements/layout/header/style.css +28 -0
- package/src/elements/layout/scroll/index.html +3 -0
- package/src/elements/layout/scroll/index.js +19 -0
- package/src/elements/layout/scroll/index.tags.json +1 -0
- package/src/elements/layout/scroll/style.css +24 -0
- package/src/elements/layout/sidebar/index.html +3 -0
- package/src/elements/layout/sidebar/index.js +24 -0
- package/src/elements/layout/sidebar/index.tags.json +1 -0
- package/src/elements/layout/sidebar/style.css +30 -0
- package/src/elements/layout/split/index.html +3 -0
- package/src/elements/layout/split/index.js +18 -0
- package/src/elements/layout/split/index.tags.json +1 -0
- package/src/elements/layout/split/style.css +28 -0
- package/src/elements/layout/stack/index.html +3 -0
- package/src/elements/layout/stack/index.js +31 -0
- package/src/elements/layout/stack/index.tags.json +1 -0
- package/src/elements/layout/stack/style.css +15 -0
- package/src/elements/layout/surface/index.html +3 -0
- package/src/elements/layout/surface/index.js +19 -0
- package/src/elements/layout/surface/index.tags.json +1 -0
- package/src/elements/layout/surface/style.css +29 -0
- package/src/elements/navigation/breadcrumb/index.html +5 -0
- package/src/elements/navigation/breadcrumb/index.js +16 -0
- package/src/elements/navigation/breadcrumb/index.tags.json +1 -0
- package/src/elements/navigation/breadcrumb/style.css +36 -0
- package/src/elements/navigation/nav/index.html +3 -0
- package/src/elements/navigation/nav/index.js +24 -0
- package/src/elements/navigation/nav/index.tags.json +1 -0
- package/src/elements/navigation/nav/style.css +38 -0
- package/src/elements/navigation/pagination/index.html +3 -0
- package/src/elements/navigation/pagination/index.js +94 -0
- package/src/elements/navigation/pagination/index.tags.json +1 -0
- package/src/elements/navigation/pagination/style.css +39 -0
- package/src/elements/navigation/steps/index.html +6 -0
- package/src/elements/navigation/steps/index.js +64 -0
- package/src/elements/navigation/steps/index.tags.json +1 -0
- package/src/elements/navigation/steps/style.css +78 -0
- package/src/elements/navigation/tabs/index.html +6 -0
- package/src/elements/navigation/tabs/index.js +132 -0
- package/src/elements/navigation/tabs/index.tags.json +1 -0
- package/src/elements/navigation/tabs/style.css +52 -0
- package/src/elements/overlay/dialog/index.html +5 -0
- package/src/elements/overlay/dialog/index.js +57 -0
- package/src/elements/overlay/dialog/index.tags.json +1 -0
- package/src/elements/overlay/dialog/style.css +31 -0
- package/src/elements/overlay/drawer/index.html +3 -0
- package/src/elements/overlay/drawer/index.js +56 -0
- package/src/elements/overlay/drawer/index.tags.json +1 -0
- package/src/elements/overlay/drawer/style.css +48 -0
- package/src/elements/overlay/menu/index.html +3 -0
- package/src/elements/overlay/menu/index.js +107 -0
- package/src/elements/overlay/menu/index.tags.json +1 -0
- package/src/elements/overlay/menu/style.css +43 -0
- package/src/elements/overlay/popover/index.html +3 -0
- package/src/elements/overlay/popover/index.js +44 -0
- package/src/elements/overlay/popover/index.tags.json +1 -0
- package/src/elements/overlay/popover/style.css +21 -0
- package/src/elements/overlay/sheet/index.html +8 -0
- package/src/elements/overlay/sheet/index.js +105 -0
- package/src/elements/overlay/sheet/index.tags.json +1 -0
- package/src/elements/overlay/sheet/style.css +64 -0
- package/src/elements/overlay/tooltip/index.html +6 -0
- package/src/elements/overlay/tooltip/index.js +16 -0
- package/src/elements/overlay/tooltip/index.tags.json +1 -0
- package/src/elements/overlay/tooltip/style.css +41 -0
- package/src/elements/primitives/avatar/index.html +2 -0
- package/src/elements/primitives/avatar/index.js +79 -0
- package/src/elements/primitives/avatar/index.tags.json +1 -0
- package/src/elements/primitives/avatar/style.css +36 -0
- package/src/elements/primitives/badge/index.html +3 -0
- package/src/elements/primitives/badge/index.js +20 -0
- package/src/elements/primitives/badge/index.tags.json +1 -0
- package/src/elements/primitives/badge/style.css +67 -0
- package/src/elements/primitives/button/index.html +3 -0
- package/src/elements/primitives/button/index.js +61 -0
- package/src/elements/primitives/button/index.tags.json +1 -0
- package/src/elements/primitives/button/style.css +66 -0
- package/src/elements/primitives/divider/index.html +1 -0
- package/src/elements/primitives/divider/index.js +43 -0
- package/src/elements/primitives/divider/index.tags.json +1 -0
- package/src/elements/primitives/divider/style.css +39 -0
- package/src/elements/primitives/icon/index.html +3 -0
- package/src/elements/primitives/icon/index.js +66 -0
- package/src/elements/primitives/icon/index.tags.json +1 -0
- package/src/elements/primitives/icon/style.css +20 -0
- package/src/elements/primitives/link/index.html +3 -0
- package/src/elements/primitives/link/index.js +129 -0
- package/src/elements/primitives/link/index.tags.json +1 -0
- package/src/elements/primitives/link/style.css +40 -0
- package/src/elements/primitives/spinner/index.html +1 -0
- package/src/elements/primitives/spinner/index.js +62 -0
- package/src/elements/primitives/spinner/index.tags.json +1 -0
- package/src/elements/primitives/spinner/style.css +20 -0
- package/src/elements/primitives/text/index.html +1 -0
- package/src/elements/primitives/text/index.js +79 -0
- package/src/elements/primitives/text/index.tags.json +1 -0
- package/src/elements/primitives/text/style.css +25 -0
- package/src/index.js +23 -0
- package/src/styles/base.css +66 -0
- package/src/styles/index.css +10 -0
- package/src/styles/layers.css +9 -0
- package/src/styles/reset.css +66 -0
- package/src/sw/activate.js +51 -0
- package/src/sw/expire.js +47 -0
- package/src/sw/index.js +28 -0
- package/src/sw/install.js +35 -0
- package/src/sw/push.js +58 -0
- package/src/sw/queue.js +60 -0
- package/src/sw/routes.js +71 -0
- package/src/sw/strategies.js +247 -0
- package/src/sw/sync.js +80 -0
- package/src/tokens/index.css +26 -0
- package/src/tokens/primitives/colors.css +54 -0
- package/src/tokens/primitives/motion.css +34 -0
- package/src/tokens/primitives/radius.css +16 -0
- package/src/tokens/primitives/shadow.css +34 -0
- package/src/tokens/primitives/spacing.css +27 -0
- package/src/tokens/primitives/typography.css +46 -0
- package/src/tokens/primitives/zindex.css +18 -0
- package/src/tokens/registered/colors.css +133 -0
- package/src/tokens/registered/dimensions.css +31 -0
- package/src/tokens/semantic/components.css +125 -0
- package/src/tokens/semantic/contrast.css +33 -0
- package/src/tokens/semantic/dark.css +61 -0
- package/src/tokens/semantic/light.css +64 -0
- package/types/core/animations/index.d.ts +52 -0
- package/types/core/api/index.d.ts +68 -0
- package/types/core/events/index.d.ts +50 -0
- package/types/core/offline/index.d.ts +68 -0
- package/types/core/platform/index.d.ts +60 -0
- package/types/core/router/index.d.ts +203 -0
- package/types/core/security/index.d.ts +33 -0
- package/types/core/state/index.d.ts +68 -0
- package/types/core/storage/index.d.ts +40 -0
- package/types/core/ui/index.d.ts +446 -0
- package/types/core/workers/index.d.ts +221 -0
- package/types/elements/index.d.ts +150 -0
- package/types/index.d.ts +18 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/security/crypto.js
|
|
3
|
+
*
|
|
4
|
+
* Web Cryptography Facade.
|
|
5
|
+
* Offloads all cryptographic functions to standard async SubtleCrypto thread pools,
|
|
6
|
+
* generating fresh IVs for AES-GCM and enforcing robust key derivation iterations.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 15 — Security Architecture §3
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns a cryptographically secure random UUID string.
|
|
13
|
+
*/
|
|
14
|
+
export function uuid() {
|
|
15
|
+
return crypto.randomUUID();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Digests data into an ArrayBuffer hash using a specified algorithm.
|
|
20
|
+
*/
|
|
21
|
+
export async function hash(data, algo = 'SHA-256') {
|
|
22
|
+
const buf = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
23
|
+
return crypto.subtle.digest(algo, buf);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates a non-extractable CryptoKey default.
|
|
28
|
+
*/
|
|
29
|
+
export async function generateKey(algo = 'AES-GCM', usages = ['encrypt', 'decrypt'], extractable = false) {
|
|
30
|
+
let param;
|
|
31
|
+
if (algo === 'AES-GCM') {
|
|
32
|
+
param = { name: 'AES-GCM', length: 256 };
|
|
33
|
+
} else if (algo === 'HMAC') {
|
|
34
|
+
param = { name: 'HMAC', hash: { name: 'SHA-256' } };
|
|
35
|
+
} else {
|
|
36
|
+
param = algo;
|
|
37
|
+
}
|
|
38
|
+
return crypto.subtle.generateKey(param, extractable, usages);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Derives an AES-GCM key from a password and salt using PBKDF2 with 600,000 iterations.
|
|
43
|
+
*/
|
|
44
|
+
export async function deriveKey(password, salt, iterations = 600000) {
|
|
45
|
+
const enc = new TextEncoder();
|
|
46
|
+
const passwordKey = await crypto.subtle.importKey(
|
|
47
|
+
'raw',
|
|
48
|
+
enc.encode(password),
|
|
49
|
+
'PBKDF2',
|
|
50
|
+
false,
|
|
51
|
+
['deriveBits', 'deriveKey']
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const saltBuf = typeof salt === 'string' ? enc.encode(salt) : salt;
|
|
55
|
+
|
|
56
|
+
return crypto.subtle.deriveKey(
|
|
57
|
+
{
|
|
58
|
+
name: 'PBKDF2',
|
|
59
|
+
salt: saltBuf,
|
|
60
|
+
iterations,
|
|
61
|
+
hash: 'SHA-256'
|
|
62
|
+
},
|
|
63
|
+
passwordKey,
|
|
64
|
+
{ name: 'AES-GCM', length: 256 },
|
|
65
|
+
false, // non-extractable by default
|
|
66
|
+
['encrypt', 'decrypt']
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Encrypts data using AES-GCM with a fresh 12-byte IV, prepending it to the ciphertext.
|
|
72
|
+
*/
|
|
73
|
+
export async function encrypt(key, data) {
|
|
74
|
+
const buf = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
75
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
76
|
+
|
|
77
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
78
|
+
{ name: 'AES-GCM', iv },
|
|
79
|
+
key,
|
|
80
|
+
buf
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Combine IV and Ciphertext for simple storage transit
|
|
84
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
85
|
+
combined.set(iv, 0);
|
|
86
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
87
|
+
return combined.buffer;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Decrypts a combined IV-ciphertext payload.
|
|
92
|
+
*/
|
|
93
|
+
export async function decrypt(key, combinedData) {
|
|
94
|
+
const view = new Uint8Array(combinedData);
|
|
95
|
+
const iv = view.slice(0, 12);
|
|
96
|
+
const ciphertext = view.slice(12);
|
|
97
|
+
|
|
98
|
+
return crypto.subtle.decrypt(
|
|
99
|
+
{ name: 'AES-GCM', iv },
|
|
100
|
+
key,
|
|
101
|
+
ciphertext
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toBase64(buffer) {
|
|
106
|
+
const bytes = new Uint8Array(buffer);
|
|
107
|
+
let binary = '';
|
|
108
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
109
|
+
return btoa(binary);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function fromBase64(b64) {
|
|
113
|
+
const binary = atob(b64);
|
|
114
|
+
const bytes = new Uint8Array(binary.length);
|
|
115
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
116
|
+
return bytes.buffer;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* String convenience over `encrypt`: encrypts text and returns a base64 string
|
|
121
|
+
* (IV + ciphertext) ready for storage or transit.
|
|
122
|
+
*/
|
|
123
|
+
export async function seal(key, text) {
|
|
124
|
+
const buffer = await encrypt(key, String(text));
|
|
125
|
+
return toBase64(buffer);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* String convenience over `decrypt`: takes a base64 string produced by `seal`
|
|
130
|
+
* and returns the decoded plaintext string.
|
|
131
|
+
*/
|
|
132
|
+
export async function unseal(key, b64) {
|
|
133
|
+
const buffer = await decrypt(key, fromBase64(b64));
|
|
134
|
+
return new TextDecoder().decode(buffer);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Signs data using HMAC or ECDSA signatures.
|
|
139
|
+
*/
|
|
140
|
+
export async function sign(key, data) {
|
|
141
|
+
const buf = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
142
|
+
const alg = key.algorithm.name === 'HMAC'
|
|
143
|
+
? { name: 'HMAC' }
|
|
144
|
+
: { name: 'ECDSA', hash: { name: 'SHA-256' } };
|
|
145
|
+
|
|
146
|
+
return crypto.subtle.sign(alg, key, buf);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Verifies a signature using HMAC or ECDSA.
|
|
151
|
+
*/
|
|
152
|
+
export async function verify(key, signature, data) {
|
|
153
|
+
const buf = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
154
|
+
const alg = key.algorithm.name === 'HMAC'
|
|
155
|
+
? { name: 'HMAC' }
|
|
156
|
+
: { name: 'ECDSA', hash: { name: 'SHA-256' } };
|
|
157
|
+
|
|
158
|
+
return crypto.subtle.verify(alg, key, signature, buf);
|
|
159
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/security/index.js
|
|
3
|
+
*
|
|
4
|
+
* Public security entry point.
|
|
5
|
+
* Aggregates SubtleCrypto operations, HTML sanitizations, and Permissions API queries.
|
|
6
|
+
*
|
|
7
|
+
* Source: doc 15 — Security Architecture §1, §3
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
uuid,
|
|
12
|
+
hash,
|
|
13
|
+
generateKey,
|
|
14
|
+
deriveKey,
|
|
15
|
+
encrypt,
|
|
16
|
+
decrypt,
|
|
17
|
+
sign,
|
|
18
|
+
verify
|
|
19
|
+
} from './crypto.js';
|
|
20
|
+
import { sanitize } from './sanitize.js';
|
|
21
|
+
import { query, watch } from './permissions.js';
|
|
22
|
+
|
|
23
|
+
export const security = {
|
|
24
|
+
uuid,
|
|
25
|
+
hash,
|
|
26
|
+
generateKey,
|
|
27
|
+
deriveKey,
|
|
28
|
+
encrypt,
|
|
29
|
+
decrypt,
|
|
30
|
+
sign,
|
|
31
|
+
verify,
|
|
32
|
+
sanitize,
|
|
33
|
+
permission: query,
|
|
34
|
+
watchPermission: watch
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
uuid,
|
|
39
|
+
hash,
|
|
40
|
+
generateKey,
|
|
41
|
+
deriveKey,
|
|
42
|
+
encrypt,
|
|
43
|
+
decrypt,
|
|
44
|
+
sign,
|
|
45
|
+
verify,
|
|
46
|
+
sanitize,
|
|
47
|
+
query as permission,
|
|
48
|
+
watch as watchPermission
|
|
49
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Security Missing Support
|
|
2
|
+
|
|
3
|
+
This document tracks remaining support, runtime gaps, type mismatches, and storage/offline synchronization improvements for `src/core/security`. Implemented behavior belongs in `usage.md`; unsupported, under-tested, or performance-gated behavior belongs here until it is built and verified.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 0. Toolchain and Naming Rules
|
|
8
|
+
|
|
9
|
+
Heavy compilation, static analysis, and code generation belong in `tools/`, not in browser runtime modules.
|
|
10
|
+
|
|
11
|
+
Current toolchain security requirements:
|
|
12
|
+
- Ensure strict CSP conformance checking at build time.
|
|
13
|
+
- Validate that all HTML insertion sites (sinks) in templates are wrapped in `sanitize` or Trusted Types.
|
|
14
|
+
|
|
15
|
+
Naming rules for security support:
|
|
16
|
+
- Prefer one-word files: `crypto.js`, `sanitize.js`, `permissions.js`, `index.js`.
|
|
17
|
+
- Plural folders: `tests/`, `types/`.
|
|
18
|
+
- Single-word methods where possible: `uuid`, `hash`, `encrypt`, `decrypt`, `sign`, `verify`, `sanitize`, `permission`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Critical Runtime and Type Gaps
|
|
23
|
+
|
|
24
|
+
### 1.1. Missing Trusted Types Integration in Sanitizer
|
|
25
|
+
- **Status**: Runtime gap.
|
|
26
|
+
- **Files**:
|
|
27
|
+
- `src/core/security/sanitize.js`
|
|
28
|
+
- **Problem**:
|
|
29
|
+
The `sanitize()` function returns a raw string. When a strict CSP enforces `require-trusted-types-for 'script'`, writing this string to DOM sinks like `innerHTML` throws a `TypeError`. The HTML sanitizer must yield a `TrustedHTML` object wrapped in a named policy (`core-sanitize`) rather than a plain string when Trusted Types are supported by the environment.
|
|
30
|
+
- **Expected Support**:
|
|
31
|
+
- Create a named Trusted Types policy `core-sanitize` inside `sanitize.js`.
|
|
32
|
+
- Wrap the sanitized HTML string using the policy to return a `TrustedHTML` object (or fall back to a plain string if the browser does not support Trusted Types).
|
|
33
|
+
- Implement a `createScriptURL(input)` policy rule to restrict dynamic script source URLs to safe origins.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Storage & Offline Integration Gaps
|
|
38
|
+
|
|
39
|
+
### 2.1. Direct crypto.randomUUID() Usage in Storage and Offline
|
|
40
|
+
- **Status**: Consistency & synchronization gap.
|
|
41
|
+
- **Files**:
|
|
42
|
+
- `src/core/offline/clock.js`
|
|
43
|
+
- `src/core/offline/queue.js`
|
|
44
|
+
- `src/core/storage/opfs.js`
|
|
45
|
+
- **Problem**:
|
|
46
|
+
Multiple components call the browser's global `crypto.randomUUID()` directly instead of importing and consuming the centralized `uuid()` utility exported by `core/security`. This prevents unified mocking/stubbing of UUID generation during unit testing and breaks consistency.
|
|
47
|
+
- **Expected Support**:
|
|
48
|
+
- Refactor `clock.js` to import and use `uuid` from `core/security`.
|
|
49
|
+
- Refactor `queue.js` to import and use `uuid` from `core/security`.
|
|
50
|
+
- Refactor `storage/opfs.js` to import and use `uuid` from `core/security`.
|
|
51
|
+
|
|
52
|
+
### 2.2. Transparent Storage Encryption Interface
|
|
53
|
+
- **Status**: Feature gap.
|
|
54
|
+
- **Files**:
|
|
55
|
+
- `src/core/storage/index.js`
|
|
56
|
+
- `src/core/security/crypto.js`
|
|
57
|
+
- **Problem**:
|
|
58
|
+
Sensitive keys or offline sync payloads stored in IndexedDB or OPFS are written as raw plaintext. A secure offline synchronization layer requires a way to transparently encrypt and decrypt values before they cross the storage boundary.
|
|
59
|
+
- **Expected Support**:
|
|
60
|
+
- Expose helper utilities in `crypto.js` to securely encrypt and decrypt objects using derived AES-GCM keys.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. Test Coverage Gaps
|
|
65
|
+
|
|
66
|
+
### 3.1. Missing Permissions Watcher Testing
|
|
67
|
+
- **Status**: Test gap.
|
|
68
|
+
- **Files**:
|
|
69
|
+
- `tests/core/security/permissions.test.js` [NEW]
|
|
70
|
+
- **Needed Coverage**:
|
|
71
|
+
- Verify that `permission()` queries return states like `'granted'`, `'denied'`, or `'prompt'`.
|
|
72
|
+
- Verify that `watchPermission()` registers a listener on permission changes, respects AbortSignal abort triggers, and cleans up event listeners correctly.
|
|
73
|
+
|
|
74
|
+
### 3.2. Missing Signature Verification Testing
|
|
75
|
+
- **Status**: Test gap.
|
|
76
|
+
- **Files**:
|
|
77
|
+
- `tests/core/security/crypto.test.js`
|
|
78
|
+
- **Needed Coverage**:
|
|
79
|
+
- Test HMAC and ECDSA key generation, message signing, and signature verification via `sign()` and `verify()`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 4. Suggested Implementation Order
|
|
84
|
+
|
|
85
|
+
1. Implement the `core-sanitize` Trusted Types policy inside `src/core/security/sanitize.js`.
|
|
86
|
+
2. Refactor direct `crypto.randomUUID()` calls in `clock.js`, `queue.js`, and `storage/opfs.js` to use `uuid` from `core/security`.
|
|
87
|
+
3. Add permission query/watcher tests in a new file `tests/core/security/permissions.test.js`.
|
|
88
|
+
4. Add sign/verify tests in `tests/core/security/crypto.test.js`.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 5. Done Criteria
|
|
93
|
+
|
|
94
|
+
This missing-support list is complete when:
|
|
95
|
+
- `sanitize()` returns a `TrustedHTML` object (when supported).
|
|
96
|
+
- Direct `crypto.randomUUID()` calls are replaced with `uuid()` imports.
|
|
97
|
+
- Permission query/watching and cryptographically signing/verifying features are fully tested.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/security/permissions.js
|
|
3
|
+
*
|
|
4
|
+
* Permissions API Facade.
|
|
5
|
+
* Queries, requests, and watches native browser permission changes,
|
|
6
|
+
* implementing AbortSignal-gated status change listeners.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 15 — Security Architecture §5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Queries the browser permission status for a given capability name.
|
|
13
|
+
*/
|
|
14
|
+
export async function query(permissionName) {
|
|
15
|
+
if (typeof navigator !== 'undefined' && navigator.permissions?.query) {
|
|
16
|
+
try {
|
|
17
|
+
// Standard descriptor format (some APIs may require additional fields, handled by consumer)
|
|
18
|
+
const status = await navigator.permissions.query({ name: permissionName });
|
|
19
|
+
return status.state; // 'granted' | 'denied' | 'prompt'
|
|
20
|
+
} catch {
|
|
21
|
+
return 'denied';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return 'denied';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subscribes to changes in a specific permission status.
|
|
29
|
+
*/
|
|
30
|
+
export function watch(permissionName, fn, signal) {
|
|
31
|
+
if (signal?.aborted) return () => {};
|
|
32
|
+
|
|
33
|
+
let activeStatus = null;
|
|
34
|
+
const listener = () => {
|
|
35
|
+
if (activeStatus) {
|
|
36
|
+
fn(activeStatus.state);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const dispose = () => {
|
|
41
|
+
if (activeStatus) {
|
|
42
|
+
activeStatus.removeEventListener('change', listener);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (typeof navigator !== 'undefined' && navigator.permissions?.query) {
|
|
47
|
+
navigator.permissions
|
|
48
|
+
.query({ name: permissionName })
|
|
49
|
+
.then((status) => {
|
|
50
|
+
if (signal?.aborted) return;
|
|
51
|
+
activeStatus = status;
|
|
52
|
+
activeStatus.addEventListener('change', listener);
|
|
53
|
+
})
|
|
54
|
+
.catch((err) => {
|
|
55
|
+
console.warn(`Unable to query permissions watch for "${permissionName}":`, err);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (signal) {
|
|
60
|
+
signal.addEventListener('abort', dispose);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return dispose;
|
|
64
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/security/sanitize.js
|
|
3
|
+
*
|
|
4
|
+
* HTML Sanitizer wrapper.
|
|
5
|
+
* Transparently targets browser Sanitizer APIs if available, falling back to a
|
|
6
|
+
* high-performance DOMParser-based secure sanitization tree that strips unapproved
|
|
7
|
+
* elements, JavaScript links, and dynamic scripting attributes.
|
|
8
|
+
*
|
|
9
|
+
* Source: doc 15 — Security Architecture §2, §4
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const ALLOWED_TAGS = new Set([
|
|
13
|
+
'a', 'abbr', 'b', 'bdi', 'bdo', 'blockquote', 'br', 'caption', 'cite', 'code',
|
|
14
|
+
'col', 'colgroup', 'data', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption',
|
|
15
|
+
'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'i', 'img',
|
|
16
|
+
'ins', 'kbd', 'li', 'mark', 'ol', 'p', 'pre', 'q', 'rp', 'rt', 'ruby', 's', 'samp',
|
|
17
|
+
'section', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot',
|
|
18
|
+
'th', 'thead', 'time', 'tr', 'u', 'ul', 'var', 'wbr'
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const ALLOWED_ATTRS = new Set([
|
|
22
|
+
'href', 'title', 'src', 'alt', 'width', 'height', 'class', 'id', 'target', 'rel', 'style'
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
let policy = null;
|
|
26
|
+
if (typeof window !== 'undefined' && window.trustedTypes) {
|
|
27
|
+
try {
|
|
28
|
+
policy = window.trustedTypes.createPolicy('core-sanitize', {
|
|
29
|
+
createHTML: (html) => html
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.warn('Trusted Types policy "core-sanitize" registration failed or already registered:', err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sanitizes an HTML string to mitigate XSS vulnerabilities.
|
|
38
|
+
*/
|
|
39
|
+
export function sanitize(html, config = {}) {
|
|
40
|
+
if (typeof window === 'undefined') return html;
|
|
41
|
+
|
|
42
|
+
let sanitized = html;
|
|
43
|
+
|
|
44
|
+
// Use experimental browser-native Sanitizer if supported
|
|
45
|
+
if (typeof globalThis.Sanitizer !== 'undefined') {
|
|
46
|
+
const sanitizer = new globalThis.Sanitizer(config);
|
|
47
|
+
const div = document.createElement('div');
|
|
48
|
+
div.setHTML(html, { sanitizer });
|
|
49
|
+
sanitized = div.innerHTML;
|
|
50
|
+
} else {
|
|
51
|
+
// Resilient fallback utilizing native Document.parseHTMLUnsafe or DOMParser
|
|
52
|
+
let doc;
|
|
53
|
+
if (typeof Document !== 'undefined' && typeof Document.parseHTMLUnsafe === 'function') {
|
|
54
|
+
doc = Document.parseHTMLUnsafe(html);
|
|
55
|
+
} else {
|
|
56
|
+
const parser = new DOMParser();
|
|
57
|
+
doc = parser.parseFromString(html, 'text/html');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const filter = (node) => {
|
|
61
|
+
if (node.nodeType === Node.TEXT_NODE) return;
|
|
62
|
+
|
|
63
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
64
|
+
const tag = node.tagName.toLowerCase();
|
|
65
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
66
|
+
node.replaceWith(document.createTextNode(''));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Purge dynamic or script attributes
|
|
71
|
+
for (const attr of [...node.attributes]) {
|
|
72
|
+
const name = attr.name.toLowerCase();
|
|
73
|
+
const value = attr.value.trim().toLowerCase();
|
|
74
|
+
|
|
75
|
+
// Evict unapproved attributes or javascript: scheme injection vectors
|
|
76
|
+
if (
|
|
77
|
+
!ALLOWED_ATTRS.has(name) ||
|
|
78
|
+
name.startsWith('on') ||
|
|
79
|
+
(name === 'href' && value.startsWith('javascript:'))
|
|
80
|
+
) {
|
|
81
|
+
node.removeAttribute(attr.name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Recursively filter descendants
|
|
86
|
+
for (const child of [...node.childNodes]) {
|
|
87
|
+
filter(child);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (const child of [...doc.body.childNodes]) {
|
|
93
|
+
filter(child);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
sanitized = doc.body.innerHTML;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return policy ? policy.createHTML(sanitized) : sanitized;
|
|
100
|
+
}
|