+
{filename ?? lang}
-
-
+
+
{children.trim()}
@@ -54,7 +54,7 @@ export function Table({ headers, rows }: TableProps) {
{headers.map((h) => (
{h}
|
@@ -67,7 +67,7 @@ export function Table({ headers, rows }: TableProps) {
{row.map((cell, j) => (
{cell}
|
diff --git a/apps/docs/aura-ui.d.ts b/apps/docs/aura-ui.d.ts
index 4f233ab..475ca60 100644
--- a/apps/docs/aura-ui.d.ts
+++ b/apps/docs/aura-ui.d.ts
@@ -1,9 +1,40 @@
import "react";
+type AuraElement> = React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ HTMLElement
+> &
+ T & { class?: string };
+
declare module "react" {
namespace JSX {
interface IntrinsicElements {
- "a-theme-provider": React.HTMLAttributes;
+ "a-theme-provider": AuraElement;
+ "a-button": AuraElement<{
+ variant?: string;
+ size?: string;
+ color?: string;
+ type?: string;
+ disabled?: boolean;
+ selected?: boolean;
+ }>;
+ "a-badge": AuraElement<{
+ variant?: string;
+ size?: string;
+ rounded?: boolean;
+ removable?: boolean;
+ }>;
+ "a-card": AuraElement<{ variant?: string; interactive?: boolean }>;
+ "a-input": AuraElement<{
+ type?: string;
+ label?: string;
+ name?: string;
+ placeholder?: string;
+ value?: string;
+ disabled?: boolean;
+ }>;
+ "a-text": AuraElement<{ variant?: string; size?: string }>;
+ "a-separator": AuraElement<{ orientation?: string }>;
}
}
}
diff --git a/apps/interface/src/utils/apis/index.ts b/apps/interface/src/utils/apis/index.ts
index 8bd393f..53b4f10 100644
--- a/apps/interface/src/utils/apis/index.ts
+++ b/apps/interface/src/utils/apis/index.ts
@@ -32,3 +32,39 @@ export const getProjects = async () => {
return (res.data! ?? []) as Project[]
}
+
+export interface VerificationSignature {
+ r: string
+ s: string
+ v: number
+}
+
+export interface VerifyProjectResult {
+ userId: string
+ projectId: number
+ client: string
+ signature: VerificationSignature
+ auraScore?: number
+ auraLevel?: number
+ verifiedAt: string
+}
+
+/**
+ * Calls the interface app's verify endpoint to generate a verification
+ * signature for a verified user. Returns the signature payload on success.
+ */
+export const verifyProject = async (
+ projectId: number,
+ payload: { userId: string; client: string; auraScore?: number; auraLevel?: number }
+) => {
+ const res = await clientAPI.POST('/projects/{id}/verify' as never, {
+ params: { path: { id: String(projectId) } },
+ body: payload
+ } as never)
+
+ if ((res as { error?: unknown }).error) {
+ throw new Error('Failed to generate verification signature')
+ }
+
+ return (res as { data?: { data?: VerifyProjectResult } }).data?.data
+}
diff --git a/bun.lock b/bun.lock
index 8d80cc8..7b39149 100644
--- a/bun.lock
+++ b/bun.lock
@@ -47,8 +47,10 @@
"@sentry/react": "^7.70.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
+ "@tanstack/query-sync-storage-persister": "5.99.0",
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-query-devtools": "^5.99.0",
+ "@tanstack/react-query-persist-client": "5.99.0",
"apisauce": "2.0.1",
"axios": "^1.5.0",
"base64-js": "^1.5.1",
@@ -160,6 +162,35 @@
"vitest-canvas-mock": "^0.3.3",
},
},
+ "apps/core": {
+ "name": "@aura/core",
+ "version": "2.1.0",
+ "dependencies": {
+ "@aura/domain": "workspace:*",
+ "@aura/ui": "workspace:*",
+ "@solid-primitives/storage": "^4.3.4",
+ "@solidjs/router": "^0.15.3",
+ "@tanstack/query-async-storage-persister": "^5.100.14",
+ "@tanstack/solid-query": "^5.100.14",
+ "@tanstack/solid-query-persist-client": "^5.100.14",
+ "bcryptjs": "^3.0.3",
+ "libphonenumber-js": "^1.13.3",
+ "localforage": "^1.10.0",
+ "qrcode": "^1.5.4",
+ "solid-js": "^1.9.5",
+ "solid-motionone": "^1.0.4",
+ "tw-animate-css": "^1.4.0",
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.18",
+ "@types/bcryptjs": "^3.0.0",
+ "@types/qrcode": "^1.5.6",
+ "tailwindcss": "^4.1.18",
+ "typescript": "^5.9.2",
+ "vite": "^6.1.0",
+ "vite-plugin-solid": "^2.11.6",
+ },
+ },
"apps/dashboard": {
"name": "aura-dashboard",
"version": "0.0.1",
@@ -242,6 +273,22 @@
"vite-tsconfig-paths": "^5.1.4",
},
},
+ "apps/demo-integration": {
+ "name": "@aura/demo-integration",
+ "version": "0.0.0",
+ "dependencies": {
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ },
+ "devDependencies": {
+ "@aura/typescript-config": "workspace:*",
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "5.9.2",
+ "vite": "^6.3.5",
+ },
+ },
"apps/docs": {
"name": "docs",
"version": "0.1.0",
@@ -325,6 +372,20 @@
"vite-tsconfig-paths": "^5.1.4",
},
},
+ "packages/domain": {
+ "name": "@aura/domain",
+ "version": "0.1.0",
+ "dependencies": {
+ "base64-js": "^1.5.1",
+ "crypto-js": "^4.2.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "tweetnacl": "^1.0.3",
+ },
+ "devDependencies": {
+ "@types/crypto-js": "^4.2.2",
+ "typescript": "~5.9.3",
+ },
+ },
"packages/eslint-config": {
"name": "@aura/eslint-config",
"version": "0.0.0",
@@ -357,6 +418,7 @@
"name": "@aura/sdk",
"version": "0.0.14-beta.1",
"dependencies": {
+ "@aura/domain": "workspace:*",
"@lit-app/state": "^1.0.0",
"@lit-labs/router": "^0.1.4",
"@lit-labs/signals": "^0.1.2",
@@ -364,7 +426,6 @@
"@zerodev/ecdsa-validator": "^5.4.9",
"@zerodev/wagmi": "^4.6.7",
"axios": "^1.13.5",
- "base64-js": "^1.5.1",
"lit": "^3.3.0",
"tweetnacl": "^1.0.3",
"viem": "^2.45.3",
@@ -393,7 +454,7 @@
},
"packages/ui": {
"name": "@aura/ui",
- "version": "0.0.1",
+ "version": "0.0.2",
"dependencies": {
"@lit/react": "^1.0.7",
"lit": "^3.3.2",
@@ -457,6 +518,12 @@
"@aura/aura": ["@aura/aura@workspace:apps/aura"],
+ "@aura/core": ["@aura/core@workspace:apps/core"],
+
+ "@aura/demo-integration": ["@aura/demo-integration@workspace:apps/demo-integration"],
+
+ "@aura/domain": ["@aura/domain@workspace:packages/domain"],
+
"@aura/eslint-config": ["@aura/eslint-config@workspace:packages/eslint-config"],
"@aura/interface": ["@aura/interface@workspace:apps/interface"],
@@ -1219,6 +1286,18 @@
"@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="],
+ "@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
+
+ "@motionone/dom": ["@motionone/dom@10.18.0", "", { "dependencies": { "@motionone/animation": "^10.18.0", "@motionone/generators": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A=="],
+
+ "@motionone/easing": ["@motionone/easing@10.18.0", "", { "dependencies": { "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg=="],
+
+ "@motionone/generators": ["@motionone/generators@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg=="],
+
+ "@motionone/types": ["@motionone/types@10.17.1", "", {}, "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A=="],
+
+ "@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="],
+
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="],
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-edAo9bW53BLYeSK+UPRr2Iz1Fj9DeGMjytvVM0HXRoo750ElWUgPsZPAOTQa12EUiwgDErH2PsFNTLvk1jBxjQ=="],
@@ -1831,6 +1910,18 @@
"@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="],
+ "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="],
+
+ "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="],
+
+ "@solid-primitives/storage": ["@solid-primitives/storage@4.3.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-GSxPAIuyxhJWOcv7n10iv3aid5oHN3KUgyA9IV0GYWlPpgyGs43aS9E85b0VXDLoH+D4ThNK8+2WEJ8B/S6Ccg=="],
+
+ "@solid-primitives/transition-group": ["@solid-primitives/transition-group@1.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-gnHS0OmcdjeoHN9n7Khu8KNrOlRc8a2weETDt2YT6o1zeW/XtUC6Db3Q9pkMU/9cCKdEmN4b0a/41MKAHRhzWA=="],
+
+ "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="],
+
+ "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
+
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
@@ -1903,18 +1994,30 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
+ "@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14", "@tanstack/query-persist-client-core": "5.100.14" } }, "sha512-otwoi4OsYoECIZBqDZ/crzyJFDXvzwCMg+T/Z+mforv9/GTqkYShddaItNC7SkFRq9VMIO3R3T71rfZfmb0NOA=="],
+
"@tanstack/query-core": ["@tanstack/query-core@5.99.0", "", {}, "sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.99.0", "", {}, "sha512-m4ufXaJ8FjWXw7xDtyzE/6fkZAyQFg9WrbMrUpt8ZecRJx58jiFOZ2lxZMphZdIpAnIeto/S8stbwLKLusyckQ=="],
+ "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.99.0", "", { "dependencies": { "@tanstack/query-core": "5.99.0" } }, "sha512-FzXoxqGYa+ozGNGBIPHJT06rJD4geiHGbVXNS8K7cwN34te64LXlO5Ep+roQYvZjXJFNrLa4W48DIYITpMxzuA=="],
+
+ "@tanstack/query-sync-storage-persister": ["@tanstack/query-sync-storage-persister@5.99.0", "", { "dependencies": { "@tanstack/query-core": "5.99.0", "@tanstack/query-persist-client-core": "5.99.0" } }, "sha512-Q7xJ+AiesEEB8ZKGrsv7qJfhyr0eT1Z+84TZxQspGZZnJziOalcLUOLGgHOxPaEQGGcxqM1NbrgGYO/WpnTjeQ=="],
+
"@tanstack/react-query": ["@tanstack/react-query@5.99.0", "", { "dependencies": { "@tanstack/query-core": "5.99.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.99.0", "", { "dependencies": { "@tanstack/query-devtools": "5.99.0" }, "peerDependencies": { "@tanstack/react-query": "^5.99.0", "react": "^18 || ^19" } }, "sha512-CqqX7LCU9yOfCY/vBURSx2YSD83ryfX+QkfkaKionTfg1s2Hdm572Ro99gW3QPoJjzvsj1HM4pnN4nbDy3MXKA=="],
+ "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.99.0", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.99.0" }, "peerDependencies": { "@tanstack/react-query": "^5.99.0", "react": "^18 || ^19" } }, "sha512-A3TTaaYZOy6dFUNJj3r10rwmUBWaXqc2N71a7jzpFXilYAYWLKJf9ivT5n5bhXsJnSkdvv1RLifUg/GdvDGaDw=="],
+
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.18", "", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A=="],
+ "@tanstack/solid-query": ["@tanstack/solid-query@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-eKW5fPWuNjGBjK9To/DNNS2b3HwTvD58T6CZbN6H0HyCjDrBOlH8q4qyJQS9gR9EXflSiivgQK+DUzg3KIHNDw=="],
+
+ "@tanstack/solid-query-persist-client": ["@tanstack/solid-query-persist-client@5.100.14", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.100.14" }, "peerDependencies": { "@tanstack/solid-query": "^5.100.14", "solid-js": "^1.6.0" } }, "sha512-CdQaaOTl9MEcfG7nJ//5rfwu0dA7W5FxIpXhRjcNYR86tKRmgKkfne98HZWpw8EGdiXYstPHClw071oNrTFgPw=="],
+
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="],
@@ -2003,6 +2106,8 @@
"@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="],
+ "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
+
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
"@types/caseless": ["@types/caseless@0.12.5", "", {}, "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="],
@@ -2461,12 +2566,16 @@
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
+ "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="],
+
"babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.15", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw=="],
"babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.14.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.6", "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ=="],
"babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.6", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.6" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A=="],
+ "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
+
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -3315,6 +3424,8 @@
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
+ "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
+
"hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
@@ -3511,6 +3622,8 @@
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
+ "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
+
"is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@@ -3635,7 +3748,7 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
- "libphonenumber-js": ["libphonenumber-js@1.12.41", "", {}, "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA=="],
+ "libphonenumber-js": ["libphonenumber-js@1.13.1", "", {}, "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA=="],
"lie": ["lie@3.1.1", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="],
@@ -3795,6 +3908,8 @@
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
+ "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
+
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
@@ -4501,6 +4616,10 @@
"serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
+ "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="],
+
+ "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="],
+
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
@@ -4561,6 +4680,12 @@
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
+ "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="],
+
+ "solid-motionone": ["solid-motionone@1.0.4", "", { "dependencies": { "@motionone/dom": "^10.17.0", "@motionone/utils": "^10.17.0", "@solid-primitives/props": "^3.1.11", "@solid-primitives/refs": "^1.0.8", "@solid-primitives/transition-group": "^1.0.5", "csstype": "^3.1.3" }, "peerDependencies": { "solid-js": "^1.8.0" } }, "sha512-aqEjgecoO9raDFznu/dEci7ORSmA26Kjj9J4Cn1Gyr0GZuOVdvsNxdxClTL9J40Aq/uYFx4GLwC8n70fMLHiuA=="],
+
+ "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
+
"sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="],
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
@@ -4969,8 +5094,12 @@
"vite-plugin-pwa": ["vite-plugin-pwa@0.21.2", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^0.2.6", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg=="],
+ "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
+
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
+ "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
+
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"vitest-canvas-mock": ["vitest-canvas-mock@0.3.3", "", { "dependencies": { "jest-canvas-mock": "~2.5.2" }, "peerDependencies": { "vitest": "*" } }, "sha512-3P968tYBpqYyzzOaVtqnmYjqbe13576/fkjbDEJSfQAkHtC5/UjuRHOhFEN/ZV5HVZIkaROBUWgazDKJ+Ibw+Q=="],
@@ -5119,6 +5248,12 @@
"@aura/aura/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+ "@aura/core/libphonenumber-js": ["libphonenumber-js@1.13.3", "", {}, "sha512-xMkdAMqcyG7iN2WZZmGIfWbYxW4orRkny+0/AXIbwL0xll2zkDX0Vzo/BXFa6+7mh2UvJl9MbcTtHk0YXkFtBA=="],
+
+ "@aura/core/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "@aura/domain/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
"@aura/eslint-config/eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
"@aura/eslint-config/eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
@@ -5129,6 +5264,8 @@
"@aura/interface/@types/ramda": ["@types/ramda@0.31.1", "", { "dependencies": { "types-ramda": "^0.31.0" } }, "sha512-Vt6sFXnuRpzaEj+yeutA0q3bcAsK7wdPuASIzR9LXqL4gJPyFw8im9qchlbp4ltuf3kDEIRmPJTD/Fkg60dn7g=="],
+ "@aura/interface/libphonenumber-js": ["libphonenumber-js@1.12.41", "", {}, "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA=="],
+
"@aura/interface/ramda": ["ramda@0.31.3", "", {}, "sha512-xKADKRNnqmDdX59PPKLm3gGmk1ZgNnj3k7DryqWwkamp4TJ6B36DdpyKEQ0EoEYmH2R62bV4Q+S0ym2z8N2f3Q=="],
"@aura/interface/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -5147,8 +5284,6 @@
"@aura/ui/vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.5", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig=="],
- "@aura/widgets/libphonenumber-js": ["libphonenumber-js@1.13.1", "", {}, "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA=="],
-
"@aura/widgets/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -5405,6 +5540,16 @@
"@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "@motionone/animation/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@motionone/dom/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@motionone/easing/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@motionone/generators/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@motionone/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
"@npmcli/fs/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -5665,6 +5810,14 @@
"@tailwindcss/vite/tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
+ "@tanstack/query-async-storage-persister/@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
+
+ "@tanstack/query-async-storage-persister/@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14" } }, "sha512-mn60cqoQO/xB6aHxp/hxlSj5mcdcTO4tjj4SXSz5MKzkaMZnvcEGySz3+cGQOT8McREN56fL41L0eR//v5RwNw=="],
+
+ "@tanstack/solid-query/@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
+
+ "@tanstack/solid-query-persist-client/@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14" } }, "sha512-mn60cqoQO/xB6aHxp/hxlSj5mcdcTO4tjj4SXSz5MKzkaMZnvcEGySz3+cGQOT8McREN56fL41L0eR//v5RwNw=="],
+
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
"@toruslabs/base-controllers/@metamask/rpc-errors": ["@metamask/rpc-errors@6.4.0", "", { "dependencies": { "@metamask/utils": "^9.0.0", "fast-safe-stringify": "^2.0.6" } }, "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg=="],
@@ -5943,6 +6096,10 @@
"aura-dashboard/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
+ "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
+
+ "babel-plugin-jsx-dom-expressions/html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
+
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
@@ -7155,6 +7312,8 @@
"@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
+ "@tanstack/solid-query-persist-client/@tanstack/query-persist-client-core/@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
+
"@toruslabs/base-controllers/@metamask/rpc-errors/@metamask/utils": ["@metamask/utils@9.3.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g=="],
"@toruslabs/base-controllers/@toruslabs/broadcast-channel/oblivious-set": ["oblivious-set@1.1.1", "", {}, "sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w=="],
diff --git a/packages/domain/package.json b/packages/domain/package.json
new file mode 100644
index 0000000..4a676d7
--- /dev/null
+++ b/packages/domain/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@aura/domain",
+ "version": "0.1.0",
+ "type": "module",
+ "description": "Framework-agnostic Aura/BrightID domain logic: crypto, recovery channels, scoring, verifications and shared types",
+ "license": "MIT",
+ "files": ["dist", "src", "package.json"],
+ "main": "./src/index.ts",
+ "module": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts",
+ "./*": "./src/*.ts"
+ },
+ "scripts": {
+ "build": "tsc --noEmit",
+ "check-types": "tsc --noEmit"
+ },
+ "dependencies": {
+ "base64-js": "^1.5.1",
+ "crypto-js": "^4.2.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "tweetnacl": "^1.0.3"
+ },
+ "devDependencies": {
+ "@types/crypto-js": "^4.2.2",
+ "typescript": "~5.9.3"
+ }
+}
diff --git a/packages/domain/src/channel.ts b/packages/domain/src/channel.ts
new file mode 100644
index 0000000..24d9813
--- /dev/null
+++ b/packages/domain/src/channel.ts
@@ -0,0 +1,100 @@
+/*
+ * Channel service client.
+ *
+ * Replaces the old `ChannelAPI` class + apisauce instance. These are plain
+ * fetch helpers — the recovery flow drives them through TanStack Query
+ * (mutation for upload, polling query for list/download) instead of calling a
+ * stateful class.
+ *
+ * POST /profile/upload/{channelId}
+ * GET /profile/list/{channelId}
+ * GET /profile/download/{channelId}/{dataId}
+ */
+
+const NO_CACHE = { 'Cache-Control': 'no-cache' };
+
+async function errorFrom(res: Response): Promise {
+ try {
+ const body = await res.json();
+ if (body?.error) return body.error as string;
+ } catch {
+ // not json
+ }
+ return `Request failed with status ${res.status}`;
+}
+
+export interface ChannelTarget {
+ /** Channel base url, e.g. `/auranode-test/profile` (proxied) */
+ channelUrl: string;
+ channelId: string;
+}
+
+export async function uploadToChannel({
+ channelUrl,
+ channelId,
+ data,
+ dataId,
+ requestedTtl,
+}: ChannelTarget & {
+ data: string;
+ dataId: string;
+ requestedTtl?: number;
+}): Promise {
+ const body = JSON.stringify({
+ data,
+ uuid: dataId,
+ // backend expects seconds
+ requestedTtl: requestedTtl ? Math.floor(requestedTtl / 1000) : undefined,
+ });
+ const res = await fetch(`${channelUrl}/upload/${channelId}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', ...NO_CACHE },
+ body,
+ });
+ if (!res.ok) throw new Error(await errorFrom(res));
+}
+
+export async function listChannel({
+ channelUrl,
+ channelId,
+}: ChannelTarget): Promise {
+ const res = await fetch(`${channelUrl}/list/${channelId}`, {
+ headers: NO_CACHE,
+ });
+ if (!res.ok) throw new Error(await errorFrom(res));
+ const json = (await res.json()) as { profileIds?: string[] };
+ if (!json?.profileIds) {
+ throw new Error(
+ `list for channel ${channelId}: unexpected response format`,
+ );
+ }
+ return json.profileIds;
+}
+
+export async function downloadFromChannel({
+ channelUrl,
+ channelId,
+ dataId,
+ deleteAfterDownload,
+}: ChannelTarget & {
+ dataId: string;
+ deleteAfterDownload?: boolean;
+}): Promise {
+ const res = await fetch(`${channelUrl}/download/${channelId}/${dataId}`, {
+ headers: NO_CACHE,
+ });
+ if (!res.ok) throw new Error(await errorFrom(res));
+ const json = (await res.json()) as { data?: string };
+ if (deleteAfterDownload) {
+ // best-effort cleanup, ignore failures
+ fetch(`${channelUrl}/${channelId}/${dataId}`, { method: 'DELETE' }).catch(
+ () => {},
+ );
+ }
+ if (!json?.data) {
+ throw new Error(
+ `download ${dataId} from channel ${channelId}: unexpected response format`,
+ );
+ }
+ return json.data;
+}
diff --git a/packages/domain/src/crypto.ts b/packages/domain/src/crypto.ts
new file mode 100644
index 0000000..6fb3200
--- /dev/null
+++ b/packages/domain/src/crypto.ts
@@ -0,0 +1,48 @@
+import { fromByteArray, toByteArray } from 'base64-js';
+import CryptoJS from 'crypto-js';
+import nacl from 'tweetnacl';
+
+export function encryptData(data: string, password: string) {
+ return CryptoJS.AES.encrypt(data, password).toString();
+}
+
+export function decryptData(data: string, password: string) {
+ return CryptoJS.AES.decrypt(data, password).toString(CryptoJS.enc.Utf8);
+}
+
+const URL_SAFE_MAP: Record = { '/': '_', '+': '-', '=': '' };
+export const b64ToUrlSafeB64 = (s: string) =>
+ s.replace(/[/+=]/g, (c) => URL_SAFE_MAP[c] ?? c);
+
+export const hash = (data: string) => {
+ const b = CryptoJS.SHA256(data).toString(CryptoJS.enc.Base64);
+ return b64ToUrlSafeB64(b);
+};
+
+export const randomWordArray = (size: number) =>
+ CryptoJS.lib.WordArray.random(size);
+
+/** Random url-safe base64 key of `bytes` length — used as the channel AES key. */
+export const urlSafeRandomKey = (bytes = 16): string =>
+ b64ToUrlSafeB64(wordArrayToB64(randomWordArray(bytes)));
+
+export const uInt8ArrayToB64 = (array: Uint8Array): string =>
+ fromByteArray(array);
+
+/** Decode a standard (non-url-safe) base64 string into bytes. */
+export const b64ToUint8Array = (str: string): Uint8Array => toByteArray(str);
+
+/** UTF-8 encode a string into bytes (matches the old app's `strToUint8Array`). */
+export const strToUint8Array = (str: string): Uint8Array =>
+ new TextEncoder().encode(str);
+
+export const wordArrayToB64 = (wa: CryptoJS.lib.WordArray) =>
+ CryptoJS.enc.Base64.stringify(wa);
+
+export const generateB64Keypair = () => {
+ const { publicKey, secretKey } = nacl.sign.keyPair();
+ return {
+ privateKey: fromByteArray(secretKey),
+ publicKey: fromByteArray(publicKey),
+ };
+};
diff --git a/packages/domain/src/globals.d.ts b/packages/domain/src/globals.d.ts
new file mode 100644
index 0000000..ce74e25
--- /dev/null
+++ b/packages/domain/src/globals.d.ts
@@ -0,0 +1,4 @@
+declare module "fast-json-stable-stringify" {
+ /** Deterministic JSON serialization with sorted object keys. */
+ export default function stringify(value: unknown): string
+}
diff --git a/packages/domain/src/http.ts b/packages/domain/src/http.ts
new file mode 100644
index 0000000..77b4708
--- /dev/null
+++ b/packages/domain/src/http.ts
@@ -0,0 +1,71 @@
+/** Default per-request ceiling so a hung server can't wedge a query forever. */
+const REQUEST_TIMEOUT_MS = 30_000
+
+/** Non-2xx response — carries the status so callers can branch on it. */
+export class HttpError extends Error {
+ readonly status: number
+
+ constructor(message: string, status: number) {
+ super(message)
+ this.name = "HttpError"
+ this.status = status
+ }
+}
+
+/**
+ * Whether an error is a 404 — e.g. a brightid profile or connections lookup
+ * for a user the aura node hasn't seen yet (profiles are created by their
+ * first received evaluation).
+ */
+export const isNotFound = (e: unknown): boolean =>
+ e instanceof HttpError && e.status === 404
+
+/**
+ * Fetch with a timeout, honoring an optional caller signal (e.g. query
+ * cancellation on unmount). Aborts on whichever fires first.
+ */
+async function fetchWithTimeout(
+ url: string,
+ init?: RequestInit,
+ signal?: AbortSignal,
+): Promise {
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS)
+ const composed = signal ? AbortSignal.any([signal, timeout]) : timeout
+ return fetch(url, { ...init, signal: composed })
+}
+
+/** GET + parse JSON, throwing on non-2xx. */
+export async function getJson(url: string, signal?: AbortSignal): Promise {
+ const res = await fetchWithTimeout(url, undefined, signal)
+ if (!res.ok)
+ throw new HttpError(`GET ${url} failed with status ${res.status}`, res.status)
+ return (await res.json()) as T
+}
+
+/** GET raw text, throwing on non-2xx (encrypted backup blobs). */
+export async function getText(url: string, signal?: AbortSignal): Promise {
+ const res = await fetchWithTimeout(url, undefined, signal)
+ if (!res.ok)
+ throw new HttpError(`GET ${url} failed with status ${res.status}`, res.status)
+ return await res.text()
+}
+
+/** POST a JSON body + parse the JSON response, throwing on non-2xx. */
+export async function postJson(
+ url: string,
+ body: unknown,
+ signal?: AbortSignal,
+): Promise {
+ const res = await fetchWithTimeout(
+ url,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ },
+ signal,
+ )
+ if (!res.ok)
+ throw new HttpError(`POST ${url} failed with status ${res.status}`, res.status)
+ return (await res.json()) as T
+}
diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts
new file mode 100644
index 0000000..b51b041
--- /dev/null
+++ b/packages/domain/src/index.ts
@@ -0,0 +1,15 @@
+export * from "./channel"
+export * from "./crypto"
+export * from "./http"
+export * from "./labels"
+export * from "./levels"
+export * from "./notifications"
+export * from "./operations"
+export * from "./passkeys"
+export * from "./recovery"
+export * from "./score"
+export * from "./types/aura"
+export * from "./types/dashboard"
+export * from "./types/evaluations"
+export * from "./verifications"
+export * from "./view-mode"
diff --git a/packages/domain/src/labels.ts b/packages/domain/src/labels.ts
new file mode 100644
index 0000000..a42c348
--- /dev/null
+++ b/packages/domain/src/labels.ts
@@ -0,0 +1,28 @@
+import { EvaluationCategory } from "./types/evaluations"
+
+/** Human label for a confidence value (1–4). */
+export const CONFIDENCE_LABELS: Record = {
+ 1: "Low",
+ 2: "Medium",
+ 3: "High",
+ 4: "Very High",
+}
+
+export const confidenceLabel = (value: number): string =>
+ CONFIDENCE_LABELS[Math.abs(value)] ?? String(Math.abs(value))
+
+/** Display label for an evaluation category. */
+export const categoryLabel: Record = {
+ [EvaluationCategory.SUBJECT]: "Subject",
+ [EvaluationCategory.PLAYER]: "Player",
+ [EvaluationCategory.TRAINER]: "Trainer",
+ [EvaluationCategory.MANAGER]: "Manager",
+}
+
+/** Who evaluates a category: subjects ← players ← trainers ← managers. */
+export const categoryEvaluatedBy: Record = {
+ [EvaluationCategory.SUBJECT]: EvaluationCategory.PLAYER,
+ [EvaluationCategory.PLAYER]: EvaluationCategory.TRAINER,
+ [EvaluationCategory.TRAINER]: EvaluationCategory.MANAGER,
+ [EvaluationCategory.MANAGER]: EvaluationCategory.MANAGER,
+}
diff --git a/packages/domain/src/levels.ts b/packages/domain/src/levels.ts
new file mode 100644
index 0000000..fe27cfe
--- /dev/null
+++ b/packages/domain/src/levels.ts
@@ -0,0 +1,15 @@
+import { EvaluationCategory } from "./types/evaluations"
+
+export const subjectLevelPoints = [
+ 0, 1_000_000, 5_000_000, 10_000_000, 150_000_000,
+]
+export const playerLevelPoints = [0, 1_000_000, 2_000_000, 3_000_000]
+export const trainerLevelPoints = [0, 500_000, 1_000_000]
+export const managerLevelPoints = [0, 1_000, 200_000]
+
+export const userLevelPoints = {
+ [EvaluationCategory.MANAGER]: managerLevelPoints,
+ [EvaluationCategory.PLAYER]: playerLevelPoints,
+ [EvaluationCategory.SUBJECT]: subjectLevelPoints,
+ [EvaluationCategory.TRAINER]: trainerLevelPoints,
+}
diff --git a/packages/domain/src/notifications.ts b/packages/domain/src/notifications.ts
new file mode 100644
index 0000000..dda4791
--- /dev/null
+++ b/packages/domain/src/notifications.ts
@@ -0,0 +1,234 @@
+import type { AuraNodeBrightIdConnection, Verifications } from "./types/aura"
+import { EvaluationCategory, EvaluationValue } from "./types/evaluations"
+import { getAuraVerification } from "./verifications"
+
+/** Thresholds from the old app's notifications store. */
+export const ALERT_THRESHOLDS = {
+ LEVEL_CHANGE: 1,
+ SCORE_CHANGE_PERCENT: 35,
+}
+
+export type NotificationKind =
+ | "evaluation"
+ | "score-increase"
+ | "score-decrease"
+ | "level-increase"
+ | "level-decrease"
+
+export interface NotificationAlert {
+ id: string
+ kind: NotificationKind
+ category: EvaluationCategory
+ /** Who the alert is about — links to their subject page. */
+ about: string
+ /** Optional second party (e.g. the subject an evaluator rated). */
+ to: string | null
+ previous: number | null
+ next: number | null
+ timestamp: number
+ viewed: boolean
+}
+
+export interface TrackedProfile {
+ level: number | null
+ score: number | null
+ /** evaluator → confidence; only tracked for outbound subjects. */
+ evaluators?: Record
+}
+
+/** Keyed by `${id}:${category}`. */
+export type TrackedProfiles = Record
+
+export const trackedKey = (id: string, category: EvaluationCategory) =>
+ `${id}:${category}`
+
+export const NOTIFICATION_CATEGORIES = [
+ EvaluationCategory.SUBJECT,
+ EvaluationCategory.PLAYER,
+ EvaluationCategory.TRAINER,
+ EvaluationCategory.MANAGER,
+]
+
+const alertId = (
+ kind: NotificationKind,
+ about: string,
+ category: EvaluationCategory,
+ timestamp: number,
+) => `${kind}:${about}:${category}:${timestamp}`
+
+function levelScoreAlerts(
+ id: string,
+ category: EvaluationCategory,
+ prev: TrackedProfile | undefined,
+ level: number | null,
+ score: number | null,
+ now: number,
+): NotificationAlert[] {
+ if (!prev) return []
+ const alerts: NotificationAlert[] = []
+
+ if (
+ level !== null &&
+ prev.level !== null &&
+ Math.abs(prev.level - level) >= ALERT_THRESHOLDS.LEVEL_CHANGE
+ ) {
+ alerts.push({
+ id: alertId(level > prev.level ? "level-increase" : "level-decrease", id, category, now),
+ kind: level > prev.level ? "level-increase" : "level-decrease",
+ category,
+ about: id,
+ to: null,
+ previous: prev.level,
+ next: level,
+ timestamp: now,
+ viewed: false,
+ })
+ }
+
+ if (
+ score !== null &&
+ prev.score !== null &&
+ prev.score !== 0 &&
+ Math.abs((score / prev.score) * 100 - 100) >=
+ ALERT_THRESHOLDS.SCORE_CHANGE_PERCENT
+ ) {
+ alerts.push({
+ id: alertId(score > prev.score ? "score-increase" : "score-decrease", id, category, now),
+ kind: score > prev.score ? "score-increase" : "score-decrease",
+ category,
+ about: id,
+ to: null,
+ previous: prev.score,
+ next: score,
+ timestamp: now,
+ viewed: false,
+ })
+ }
+
+ return alerts
+}
+
+/**
+ * Pure diff of fresh node data against the previously tracked snapshot.
+ * Produces alerts for: own level/score changes per category, new inbound
+ * evaluations of the user, and changes on subjects the user has evaluated
+ * (their level/score plus other evaluators' new or changed ratings).
+ *
+ * The first run (`lastFetch === null`) only seeds the tracked snapshot and
+ * yields no alerts — otherwise every existing evaluation would fire at once.
+ * No I/O and no clock access: the caller passes `now`.
+ */
+export function diffNotifications(params: {
+ subjectId: string
+ /** The user's own verifications (from their profile). */
+ verifications: Verifications | undefined
+ /** The user's inbound connections (evaluations of them ride along). */
+ inbound: AuraNodeBrightIdConnection[]
+ /** The user's outbound connections (subjects they evaluated). */
+ outbound: AuraNodeBrightIdConnection[]
+ prevTracked: TrackedProfiles
+ lastFetch: number | null
+ now: number
+}): { alerts: NotificationAlert[]; tracked: TrackedProfiles } {
+ const { subjectId, verifications, inbound, outbound, prevTracked, lastFetch, now } = params
+
+ const seedOnly = lastFetch === null
+ const tracked: TrackedProfiles = { ...prevTracked }
+ const alerts: NotificationAlert[] = []
+
+ // ── Own level/score per category ─────────────────────────
+ for (const category of NOTIFICATION_CATEGORIES) {
+ const v = getAuraVerification(verifications, category)
+ const key = trackedKey(subjectId, category)
+ if (!seedOnly) {
+ alerts.push(
+ ...levelScoreAlerts(
+ subjectId,
+ category,
+ tracked[key],
+ v?.level ?? null,
+ v?.score ?? null,
+ now,
+ ),
+ )
+ }
+ tracked[key] = { level: v?.level ?? null, score: v?.score ?? null }
+ }
+
+ // ── New inbound evaluations of the user ──────────────────
+ if (!seedOnly) {
+ for (const connection of inbound) {
+ for (const e of connection.auraEvaluations ?? []) {
+ const modified = e.modified || connection.timestamp
+ if (modified <= (lastFetch ?? 0)) continue
+ alerts.push({
+ id: alertId("evaluation", connection.id, e.category, modified),
+ kind: "evaluation",
+ category: e.category,
+ about: connection.id,
+ to: subjectId,
+ previous: null,
+ next:
+ (e.evaluation === EvaluationValue.POSITIVE ? 1 : -1) * e.confidence,
+ timestamp: modified,
+ viewed: false,
+ })
+ }
+ }
+ }
+
+ // ── Subjects the user evaluated ──────────────────────────
+ for (const subject of outbound) {
+ const categories = new Set(
+ (subject.auraEvaluations ?? []).map((e) => e.category),
+ )
+ for (const category of categories) {
+ const v = getAuraVerification(subject.verifications, category)
+ const key = trackedKey(subject.id, category)
+ const prev = tracked[key]
+
+ const evaluators: Record = {}
+ for (const impact of v?.impacts ?? []) {
+ evaluators[impact.evaluator] = impact.confidence
+ }
+
+ if (prev && !seedOnly) {
+ alerts.push(
+ ...levelScoreAlerts(
+ subject.id,
+ category,
+ prev,
+ v?.level ?? null,
+ v?.score ?? null,
+ now,
+ ),
+ )
+
+ // Other evaluators newly rating (or re-rating) this subject.
+ for (const impact of v?.impacts ?? []) {
+ if (impact.evaluator === subjectId) continue
+ if (prev.evaluators?.[impact.evaluator] === impact.confidence) continue
+ alerts.push({
+ id: alertId("evaluation", impact.evaluator, category, impact.modified),
+ kind: "evaluation",
+ category,
+ about: impact.evaluator,
+ to: subject.id,
+ previous: prev.evaluators?.[impact.evaluator] ?? null,
+ next: impact.confidence,
+ timestamp: impact.modified,
+ viewed: false,
+ })
+ }
+ }
+
+ tracked[key] = {
+ level: v?.level ?? null,
+ score: v?.score ?? null,
+ evaluators,
+ }
+ }
+ }
+
+ return { alerts, tracked }
+}
diff --git a/packages/domain/src/operations.ts b/packages/domain/src/operations.ts
new file mode 100644
index 0000000..7f2b7ff
--- /dev/null
+++ b/packages/domain/src/operations.ts
@@ -0,0 +1,143 @@
+import stringify from "fast-json-stable-stringify"
+import nacl from "tweetnacl"
+import { hash, strToUint8Array, uInt8ArrayToB64 } from "./crypto"
+import { getJson, postJson } from "./http"
+import type {
+ EvaluateOperation,
+ EvaluationCategory,
+ EvaluationValue,
+ OperationState,
+} from "./types/evaluations"
+
+/** BrightID operation protocol version (constant in the old app). */
+const OP_VERSION = 6
+
+/**
+ * The signed "Evaluate" operation exactly as the node canonicalizes and hashes
+ * it. Key set and value types must match the server's expectation byte-for-byte
+ * — `fast-json-stable-stringify` sorts keys, so the serialized message is
+ * identical regardless of insertion order here.
+ */
+export interface SignedEvaluateOp {
+ name: "Evaluate"
+ evaluator: string
+ evaluated: string
+ evaluation: EvaluationValue
+ confidence: number
+ domain: "BrightID"
+ category: EvaluationCategory
+ timestamp: number
+ v: number
+ sig: string
+}
+
+export interface BuildEvaluateOperationParams {
+ evaluator: string
+ evaluated: string
+ evaluation: EvaluationValue
+ confidence: number
+ category: EvaluationCategory
+ timestamp: number
+ /** Ed25519 secret key bytes (caller decodes the stored b64 key). */
+ secretKey: Uint8Array
+}
+
+/**
+ * Build and sign an "Evaluate" operation. Pure: no I/O. Returns the signed op
+ * plus the canonical `message` it was signed over (also what the node hashes),
+ * so the caller can verify the returned hash without re-serializing.
+ */
+export function buildEvaluateOperation(params: BuildEvaluateOperationParams): {
+ op: SignedEvaluateOp
+ message: string
+} {
+ const unsigned = {
+ name: "Evaluate" as const,
+ evaluator: params.evaluator,
+ evaluated: params.evaluated,
+ evaluation: params.evaluation,
+ confidence: params.confidence,
+ domain: "BrightID" as const,
+ category: params.category,
+ timestamp: params.timestamp,
+ v: OP_VERSION,
+ }
+
+ const message = stringify(unsigned)
+ const sig = uInt8ArrayToB64(
+ nacl.sign.detached(strToUint8Array(message), params.secretKey),
+ )
+
+ return { op: { ...unsigned, sig }, message }
+}
+
+/**
+ * Build, sign, and submit an "Evaluate" operation to the node, returning an
+ * `EvaluateOperation` the operations store can track. The node independently
+ * canonicalizes the op and returns its hash; we verify it equals `hash(message)`
+ * (SHA256 -> url-safe b64) and throw on mismatch so a rejected op never lands in
+ * the store as if accepted.
+ */
+export async function submitEvaluateOperation(
+ nodeUrl: string,
+ params: BuildEvaluateOperationParams,
+ signal?: AbortSignal,
+): Promise {
+ const { op, message } = buildEvaluateOperation(params)
+
+ const res = await postJson<{ data: { hash: string } }>(
+ `${nodeUrl}/operations`,
+ op,
+ signal,
+ )
+
+ const returnedHash = res.data?.hash
+ if (returnedHash !== hash(message)) {
+ throw new Error("Invalid operation hash returned from server")
+ }
+
+ return {
+ hash: returnedHash,
+ evaluator: op.evaluator,
+ evaluated: op.evaluated,
+ category: op.category,
+ confidence: op.confidence,
+ evaluation: op.evaluation,
+ timestamp: op.timestamp,
+ postTimestamp: Date.now(),
+ state: "INIT",
+ }
+}
+
+/**
+ * The node reports operation states in lowercase; the domain `OperationState`
+ * is uppercase. Map known values, defaulting unexpected strings to `"UNKNOWN"`.
+ */
+const NODE_STATE_TO_OPERATION_STATE: Record = {
+ unknown: "UNKNOWN",
+ init: "INIT",
+ sent: "SENT",
+ applied: "APPLIED",
+ failed: "FAILED",
+ expired: "EXPIRED",
+}
+
+/**
+ * Fetch the current state of a submitted operation from the node. GETs
+ * `${nodeUrl}/operations/${hash}` (response shape `{ data: { state, result } }`,
+ * matching the old `getOperationState`) and normalizes the lowercase node state
+ * to the uppercase domain `OperationState`. Pure aside from the GET; unknown or
+ * missing states resolve to `"UNKNOWN"`. Throws only on transport errors.
+ */
+export async function fetchOperationState(
+ nodeUrl: string,
+ hash: string,
+ signal?: AbortSignal,
+): Promise {
+ const res = await getJson<{ data?: { state?: string } }>(
+ `${nodeUrl}/operations/${hash}`,
+ signal,
+ )
+ const nodeState = res.data?.state ?? ""
+ return NODE_STATE_TO_OPERATION_STATE[nodeState] ?? "UNKNOWN"
+}
diff --git a/packages/domain/src/passkeys.ts b/packages/domain/src/passkeys.ts
new file mode 100644
index 0000000..746f06e
--- /dev/null
+++ b/packages/domain/src/passkeys.ts
@@ -0,0 +1,176 @@
+import nacl from "tweetnacl"
+import {
+ b64ToUint8Array,
+ b64ToUrlSafeB64,
+ strToUint8Array,
+ uInt8ArrayToB64,
+} from "./crypto"
+
+/**
+ * Passkey-derived identity. The Ed25519 keypair is derived deterministically
+ * from the passkey's PRF output, so the same passkey always yields the same
+ * identity — no server-side account storage is needed.
+ */
+export interface PasskeyIdentity {
+ /** Url-safe b64 of the public key — used as the user's id on the node. */
+ id: string
+ /** Standard b64 (same shape as `generateB64Keypair`). */
+ publicKey: string
+ privateKey: string
+}
+
+interface PRFExtensionResult {
+ prf?: { results?: { first?: ArrayBuffer } }
+}
+
+const LS_CRED_ID = "aura_passkey_cred_id"
+const LS_PUB_KEY = "aura_passkey_pub_key"
+
+// Derivation constants — must never change, or existing passkeys would map to
+// different identities. Shared with the interface app's passkey login.
+const PRF_SALT = "BrightID"
+const HKDF_INFO = "BrightID Ed25519 Identity v1"
+
+const PRF_UNSUPPORTED_MESSAGE =
+ "This device's passkeys don't support key derivation (PRF). " +
+ "Try Chrome on desktop, or Safari 17.5+ on iOS."
+
+async function hkdf(inputKeyMaterial: Uint8Array): Promise {
+ const baseKey = await crypto.subtle.importKey(
+ "raw",
+ inputKeyMaterial as BufferSource,
+ { name: "HKDF" },
+ false,
+ ["deriveBits"],
+ )
+ const derived = await crypto.subtle.deriveBits(
+ {
+ name: "HKDF",
+ hash: "SHA-256",
+ salt: strToUint8Array(PRF_SALT) as BufferSource,
+ info: strToUint8Array(HKDF_INFO) as BufferSource,
+ },
+ baseKey,
+ 32 * 8,
+ )
+ return new Uint8Array(derived)
+}
+
+function identityFromSeed(seed: Uint8Array): PasskeyIdentity {
+ const keypair = nacl.sign.keyPair.fromSeed(seed)
+ // nacl copies the seed; zero our copy so it doesn't linger in memory
+ seed.fill(0)
+ const publicKey = uInt8ArrayToB64(keypair.publicKey)
+ return {
+ id: b64ToUrlSafeB64(publicKey),
+ publicKey,
+ privateKey: uInt8ArrayToB64(keypair.secretKey),
+ }
+}
+
+function rememberCredential(rawId: ArrayBuffer, publicKey: string): void {
+ localStorage.setItem(LS_CRED_ID, uInt8ArrayToB64(new Uint8Array(rawId)))
+ localStorage.setItem(LS_PUB_KEY, publicKey)
+}
+
+/** Whether a passkey was already registered from this device/browser. */
+export function hasPasskeyCredential(): boolean {
+ return !!localStorage.getItem(LS_CRED_ID)
+}
+
+/** Forget the locally remembered credential (does not delete the passkey). */
+export function clearPasskeyCredential(): void {
+ localStorage.removeItem(LS_CRED_ID)
+ localStorage.removeItem(LS_PUB_KEY)
+}
+
+/**
+ * Create a brand-new passkey and derive an identity from it.
+ *
+ * Some authenticators return the PRF output during creation itself (one tap);
+ * others only evaluate PRF during authentication, so we fall back to an
+ * immediate assertion against the freshly created credential.
+ */
+export async function createPasskeyIdentity(
+ username: string,
+): Promise {
+ const credential = (await navigator.credentials.create({
+ publicKey: {
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
+ rp: { name: "Aura" },
+ user: {
+ id: crypto.getRandomValues(new Uint8Array(16)),
+ name: username,
+ displayName: username,
+ },
+ pubKeyCredParams: [
+ { type: "public-key", alg: -7 }, // ES256
+ { type: "public-key", alg: -257 }, // RS256
+ ],
+ authenticatorSelection: {
+ userVerification: "required",
+ residentKey: "required",
+ },
+ extensions: {
+ prf: { eval: { first: strToUint8Array(PRF_SALT) } },
+ } as AuthenticationExtensionsClientInputs,
+ },
+ })) as PublicKeyCredential | null
+
+ if (!credential) throw new Error("Passkey creation was cancelled")
+
+ // Persist the credential id so the fallback assertion targets it
+ localStorage.setItem(
+ LS_CRED_ID,
+ uInt8ArrayToB64(new Uint8Array(credential.rawId)),
+ )
+
+ const extensions = credential.getClientExtensionResults() as PRFExtensionResult
+ const prfFromCreate = extensions.prf?.results?.first
+ if (!prfFromCreate) return getPasskeyIdentity()
+
+ const identity = identityFromSeed(await hkdf(new Uint8Array(prfFromCreate)))
+ rememberCredential(credential.rawId, identity.publicKey)
+ return identity
+}
+
+/**
+ * Re-derive the identity from an existing passkey (one tap). Works without a
+ * locally remembered credential too — the browser then offers any resident
+ * passkey for this origin, which lets a user sign in on a new browser profile.
+ */
+export async function getPasskeyIdentity(): Promise {
+ const savedId = localStorage.getItem(LS_CRED_ID)
+ const savedPubKey = localStorage.getItem(LS_PUB_KEY)
+
+ const assertion = (await navigator.credentials.get({
+ publicKey: {
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
+ allowCredentials: savedId
+ ? [{ id: b64ToUint8Array(savedId) as BufferSource, type: "public-key" }]
+ : [],
+ userVerification: "required",
+ extensions: {
+ prf: { eval: { first: strToUint8Array(PRF_SALT) } },
+ } as AuthenticationExtensionsClientInputs,
+ },
+ })) as PublicKeyCredential | null
+
+ if (!assertion) throw new Error("Passkey prompt was cancelled")
+
+ const extensions = assertion.getClientExtensionResults() as PRFExtensionResult
+ const prfBytes = extensions.prf?.results?.first
+ if (!prfBytes) throw new Error(PRF_UNSUPPORTED_MESSAGE)
+
+ const identity = identityFromSeed(await hkdf(new Uint8Array(prfBytes)))
+
+ if (savedPubKey && savedPubKey !== identity.publicKey) {
+ throw new Error(
+ "Wrong passkey selected — this would produce a different identity. " +
+ "Please use the passkey you originally registered with.",
+ )
+ }
+
+ rememberCredential(assertion.rawId, identity.publicKey)
+ return identity
+}
diff --git a/packages/domain/src/recovery.ts b/packages/domain/src/recovery.ts
new file mode 100644
index 0000000..5d8f7cd
--- /dev/null
+++ b/packages/domain/src/recovery.ts
@@ -0,0 +1,94 @@
+import {
+ downloadFromChannel,
+ listChannel,
+ uploadToChannel,
+} from "./channel"
+import { b64ToUrlSafeB64, decryptData, hash } from "./crypto"
+
+export const RECOVERY_CHANNEL_TTL = 24 * 60 * 60 * 1000 // 1 day
+export const CHANNEL_POLL_INTERVAL = 3000
+
+const IMPORT_PREFIX = "sig_"
+const QR_TYPE_ADD_SUPER_USER_APP = "5"
+
+export interface RecoveredUser {
+ id: string
+ name?: string
+ password: string
+}
+
+/** Build the channel url embedded in the QR code. */
+export function buildRecoveryChannelQrUrl({
+ aesKey,
+ href,
+ name,
+}: {
+ aesKey: string
+ href: string
+ name?: string
+}): string {
+ const url = new URL(href)
+ url.searchParams.append("aes", aesKey)
+ url.searchParams.append("t", QR_TYPE_ADD_SUPER_USER_APP)
+ if (name) url.searchParams.append("n", name)
+ url.searchParams.append("p", "false")
+ return url.href
+}
+
+/** Upload our recovery signing key to the freshly created channel. */
+export async function uploadRecoveryData({
+ channelUrl,
+ aesKey,
+ publicKey,
+ timestamp,
+}: {
+ channelUrl: string
+ aesKey: string
+ publicKey: string
+ timestamp: number
+}): Promise {
+ const channelId = hash(aesKey)
+ const data = JSON.stringify({ signingKey: publicKey, timestamp })
+ await uploadToChannel({
+ channelUrl,
+ channelId,
+ data,
+ dataId: "data",
+ requestedTtl: RECOVERY_CHANNEL_TTL,
+ })
+}
+
+/**
+ * Poll the channel once: look for the encrypted user-info the scanner uploaded
+ * and, if present, decrypt and return it. Returns null until the phone scans.
+ */
+export async function pollRecoveredUser({
+ channelUrl,
+ channelId,
+ aesKey,
+ signingKey,
+}: {
+ channelUrl: string
+ channelId: string
+ aesKey: string
+ signingKey: string
+}): Promise {
+ const dataIds = await listChannel({ channelUrl, channelId })
+
+ const prefix = `${IMPORT_PREFIX}userinfo_`
+ const self = b64ToUrlSafeB64(signingKey)
+ const dataId = dataIds.find(
+ (id) =>
+ id.startsWith(prefix) && id.replace(prefix, "").split(":")[1] !== self,
+ )
+ if (!dataId) return null
+
+ const encrypted = await downloadFromChannel({
+ channelUrl,
+ channelId,
+ dataId,
+ deleteAfterDownload: true,
+ })
+ const info = JSON.parse(decryptData(encrypted, aesKey)) as RecoveredUser
+ return info
+}
diff --git a/packages/domain/src/score.ts b/packages/domain/src/score.ts
new file mode 100644
index 0000000..b1d6dd4
--- /dev/null
+++ b/packages/domain/src/score.ts
@@ -0,0 +1,75 @@
+import { userLevelPoints } from "./levels"
+import type { EvaluationCategory } from "./types/evaluations"
+import type { AuraImpactRaw } from "./types/aura"
+
+export const calculateImpact = (score: number, rating: number) =>
+ rating > 0 ? score * rating : rating * score * 4
+
+export const calculateImpactPercent = (
+ impacts: AuraImpactRaw[],
+ score: number,
+) => {
+ const sumImpacts = impacts.reduce((p, c) => Math.abs(c.impact) + p, 0)
+ return sumImpacts ? score / sumImpacts : 0
+}
+
+export const calculateRemainingScoreToNextLevel = (
+ view: EvaluationCategory,
+ score: number,
+) => {
+ const levels = userLevelPoints[view]
+ const nextLevelStart = levels.find((item) => item > score) ?? levels.at(-1)!
+ return nextLevelStart - score
+}
+
+export const maximumScoreTobeReached = 4_000_000_000
+
+export const progressSections = [
+ 0.00000875, 0.000125, 0.025, 0.075, 0.2, 0.375, 0.625, 1,
+]
+
+/** 0–100 progress of a score across the non-linear level sections. */
+export const calculateUserScorePercentage = (
+ _view: EvaluationCategory,
+ score: number,
+) => {
+ if (score < 0) return -1
+ if (score === 0) return 0
+ if (score > maximumScoreTobeReached) return 100
+
+ const sectionsPassed = progressSections.filter(
+ (pct) => pct * maximumScoreTobeReached < score,
+ )
+ const currentSection =
+ (progressSections[sectionsPassed.length] ?? 1) * maximumScoreTobeReached
+
+ return (
+ (sectionsPassed.length / progressSections.length) * 100 +
+ (score / currentSection) * progressSections.length
+ )
+}
+
+/** Level index for a category given its impact ratings. */
+export const calculateSubjectScore = (
+ category: EvaluationCategory,
+ ratings: AuraImpactRaw[],
+) => {
+ const levels = userLevelPoints[category]
+ const score = ratings.reduce((p, item) => (item.score ?? 0) + p, 0)
+ return levels.findIndex((item) => item > score)
+}
+
+/**
+ * One evaluator's share of a subject's total absolute impact, as a rounded
+ * percentage — null when the evaluator has no impact or there is none at all.
+ */
+export const impactShare = (
+ impacts: AuraImpactRaw[] | null | undefined,
+ evaluator: string | undefined,
+): number | null => {
+ if (!impacts?.length || !evaluator) return null
+ const total = impacts.reduce((sum, i) => sum + Math.abs(i.impact), 0)
+ const mine = impacts.find((i) => i.evaluator === evaluator)?.impact
+ if (!mine || !total) return null
+ return Math.round((Math.abs(mine) / total) * 100)
+}
diff --git a/packages/domain/src/types/aura.ts b/packages/domain/src/types/aura.ts
new file mode 100644
index 0000000..37d91fa
--- /dev/null
+++ b/packages/domain/src/types/aura.ts
@@ -0,0 +1,116 @@
+import type { EvaluationCategory, EvaluationValue } from "./evaluations"
+
+/** BrightID verification / domain types (aura node `?withVerifications=true`). */
+export interface AuraImpactRaw {
+ evaluator: string
+ level?: number | null
+ score: number | null
+ confidence: number
+ impact: number
+ modified: number
+}
+
+export type Domain = {
+ name: "BrightID"
+ categories: {
+ name: EvaluationCategory
+ score: number
+ level: number
+ impacts: AuraImpactRaw[]
+ }[]
+}
+
+export type Verifications = {
+ name: string
+ block: number
+ timestamp: number
+ domains?: Domain[]
+}[]
+
+export interface BrightIdProfile {
+ createdAt: number
+ verifications: Verifications
+}
+
+/** Connections + evaluations from the aura node. */
+export type ConnectionLevel =
+ | "reported"
+ | "suspicious"
+ | "just met"
+ | "already known"
+ | "recovery"
+ | "aura only"
+
+export type BrightIdConnection = {
+ id: string
+ level: ConnectionLevel
+ reportReason: string | null
+ timestamp: number
+}
+
+export type AuraEvaluation = {
+ evaluation: EvaluationValue
+ confidence: number
+ domain: "BrightID"
+ category: EvaluationCategory
+ modified: number
+ timestamp: number
+}
+
+export type AuraNodeBrightIdConnection = BrightIdConnection & {
+ auraEvaluations?: AuraEvaluation[]
+ verifications: Verifications
+}
+
+export type AuraNodeConnectionsResponse = {
+ data: {
+ connections: AuraNodeBrightIdConnection[]
+ }
+}
+
+/** A rating derived from a connection's aura evaluations (or a pending op). */
+export type AuraRating = {
+ id?: number
+ toBrightId: string
+ fromBrightId: string
+ rating: string
+ timestamp: number
+ createdAt: string
+ updatedAt: string
+ category: EvaluationCategory
+ isPending: boolean
+ verifications?: Verifications
+}
+
+/** Connections as stored in the (decrypted) BrightID backup. */
+export type BrightIdBackupConnection = BrightIdConnection &
+ Partial<{
+ name: string
+ connectionDate: number
+ photo: { filename: string }
+ status: "verified"
+ notificationToken: string
+ socialMedia: unknown[]
+ verifications: Verifications
+ incomingLevel: ConnectionLevel
+ }>
+
+export type AuraNodeBrightIdConnectionWithBackupData =
+ AuraNodeBrightIdConnection & BrightIdBackupConnection
+
+export type BrightIdBackup = {
+ userData: {
+ id: string
+ name: string
+ photo: { filename: string }
+ }
+ connections: BrightIdBackupConnection[]
+ groups: unknown[]
+}
+
+export type BrightIdBackupWithAuraConnectionData = Omit<
+ BrightIdBackup,
+ "connections"
+> & {
+ connections: AuraNodeBrightIdConnectionWithBackupData[]
+}
diff --git a/packages/domain/src/types/dashboard.ts b/packages/domain/src/types/dashboard.ts
new file mode 100644
index 0000000..d3c120b
--- /dev/null
+++ b/packages/domain/src/types/dashboard.ts
@@ -0,0 +1,10 @@
+/** Which role lens the user is viewing the app through. */
+export enum PreferredView {
+ PLAYER,
+ TRAINER,
+ MANAGER_EVALUATING_TRAINER,
+ MANAGER_EVALUATING_MANAGER,
+}
+
+
+
diff --git a/packages/domain/src/types/evaluations.ts b/packages/domain/src/types/evaluations.ts
new file mode 100644
index 0000000..43c6ef7
--- /dev/null
+++ b/packages/domain/src/types/evaluations.ts
@@ -0,0 +1,31 @@
+export enum EvaluationCategory {
+ SUBJECT = "subject",
+ PLAYER = "player",
+ TRAINER = "trainer",
+ MANAGER = "manager",
+}
+
+export enum EvaluationValue {
+ POSITIVE = "positive",
+ NEGATIVE = "negative",
+}
+
+export type OperationState =
+ | "UNKNOWN"
+ | "INIT"
+ | "SENT"
+ | "APPLIED"
+ | "FAILED"
+ | "EXPIRED"
+
+export interface EvaluateOperation {
+ hash: string
+ evaluator: string
+ evaluated: string
+ category: EvaluationCategory
+ confidence: number
+ evaluation: EvaluationValue
+ timestamp: number
+ postTimestamp?: number
+ state: OperationState
+}
diff --git a/packages/domain/src/verifications.ts b/packages/domain/src/verifications.ts
new file mode 100644
index 0000000..bde48a3
--- /dev/null
+++ b/packages/domain/src/verifications.ts
@@ -0,0 +1,34 @@
+import type { EvaluationCategory } from "./types/evaluations"
+import type { Verifications } from "./types/aura"
+
+export const getUserHasRecovery = (verifications?: Verifications) =>
+ verifications
+ ? !!verifications.find((v) => v.name === "SocialRecoverySetup")
+ : null
+
+/** The Aura → BrightID domain category entry for a given evaluation category. */
+export const getAuraVerification = (
+ verifications: Verifications | undefined,
+ category: EvaluationCategory,
+) => {
+ if (!verifications) return null
+ return verifications
+ .find((v) => v.name === "Aura")
+ ?.domains?.find((d) => d.name === "BrightID")
+ ?.categories.find((c) => c.name === category)
+}
+
+/** Extract level/score for a subject in a category from their verifications. */
+export function parseVerifications(
+ verifications: Verifications | undefined,
+ category: EvaluationCategory,
+) {
+ const auraVerification = getAuraVerification(verifications, category)
+ return {
+ userHasRecovery: getUserHasRecovery(verifications),
+ auraVerification,
+ auraLevel: auraVerification?.level ?? null,
+ auraScore: auraVerification?.score ?? null,
+ auraImpacts: auraVerification?.impacts ?? null,
+ }
+}
diff --git a/packages/domain/src/view-mode.ts b/packages/domain/src/view-mode.ts
new file mode 100644
index 0000000..efa617b
--- /dev/null
+++ b/packages/domain/src/view-mode.ts
@@ -0,0 +1,57 @@
+import { EvaluationCategory } from "./types/evaluations"
+import { PreferredView } from "./types/dashboard"
+
+/** The role we *evaluate* when in a given view. */
+export const viewModeToViewAs: Record = {
+ [PreferredView.PLAYER]: EvaluationCategory.SUBJECT,
+ [PreferredView.TRAINER]: EvaluationCategory.PLAYER,
+ [PreferredView.MANAGER_EVALUATING_TRAINER]: EvaluationCategory.TRAINER,
+ [PreferredView.MANAGER_EVALUATING_MANAGER]: EvaluationCategory.MANAGER,
+}
+
+/** The view that evaluates the current view's role (one level up). */
+export const viewModeToEvaluatorViewMode: Record =
+ {
+ [PreferredView.PLAYER]: PreferredView.TRAINER,
+ [PreferredView.TRAINER]: PreferredView.MANAGER_EVALUATING_TRAINER,
+ [PreferredView.MANAGER_EVALUATING_TRAINER]:
+ PreferredView.MANAGER_EVALUATING_MANAGER,
+ [PreferredView.MANAGER_EVALUATING_MANAGER]:
+ PreferredView.MANAGER_EVALUATING_MANAGER,
+ }
+
+/** `?viewas=` query value → view. */
+export const viewAsToViewMode: Record = {
+ [EvaluationCategory.SUBJECT]: PreferredView.PLAYER,
+ [EvaluationCategory.PLAYER]: PreferredView.TRAINER,
+ [EvaluationCategory.TRAINER]: PreferredView.MANAGER_EVALUATING_TRAINER,
+ [EvaluationCategory.MANAGER]: PreferredView.MANAGER_EVALUATING_MANAGER,
+}
+
+export const viewModeToString: Record = {
+ [PreferredView.PLAYER]: "Player",
+ [PreferredView.TRAINER]: "Trainer",
+ [PreferredView.MANAGER_EVALUATING_TRAINER]: "Manager",
+ [PreferredView.MANAGER_EVALUATING_MANAGER]: "Manager",
+}
+
+/** URL slug (`/home/:view`) ↔ view. Manager collapses to one slug. */
+export const viewSlugToViewMode: Record = {
+ player: PreferredView.PLAYER,
+ trainer: PreferredView.TRAINER,
+ manager: PreferredView.MANAGER_EVALUATING_TRAINER,
+}
+
+export const viewModeToSlug: Record = {
+ [PreferredView.PLAYER]: "player",
+ [PreferredView.TRAINER]: "trainer",
+ [PreferredView.MANAGER_EVALUATING_TRAINER]: "manager",
+ [PreferredView.MANAGER_EVALUATING_MANAGER]: "manager",
+}
+
+export const viewModeSubjectString: Record = {
+ [PreferredView.PLAYER]: "Subject",
+ [PreferredView.TRAINER]: "Player",
+ [PreferredView.MANAGER_EVALUATING_TRAINER]: "Trainer",
+ [PreferredView.MANAGER_EVALUATING_MANAGER]: "Manager",
+}
diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json
new file mode 100644
index 0000000..7dfa1c4
--- /dev/null
+++ b/packages/domain/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../typescript-config/base.json",
+ "compilerOptions": {
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "target": "ES2022",
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src"]
+}
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 377b8c6..e9349db 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -55,6 +55,7 @@
}
},
"dependencies": {
+ "@aura/domain": "workspace:*",
"@lit-app/state": "^1.0.0",
"@lit-labs/router": "^0.1.4",
"@lit-labs/signals": "^0.1.2",
@@ -62,7 +63,6 @@
"@zerodev/ecdsa-validator": "^5.4.9",
"@zerodev/wagmi": "^4.6.7",
"axios": "^1.13.5",
- "base64-js": "^1.5.1",
"lit": "^3.3.0",
"tweetnacl": "^1.0.3",
"viem": "^2.45.3",
diff --git a/packages/sdk/src/apis/dashboard.ts b/packages/sdk/src/apis/dashboard.ts
new file mode 100644
index 0000000..94c3b5b
--- /dev/null
+++ b/packages/sdk/src/apis/dashboard.ts
@@ -0,0 +1,62 @@
+import axios from "axios"
+
+// Dashboard API base URL - using environment variable for flexibility
+const DASHBOARD_API_BASE = import.meta.env.VITE_SOME_AURA_DASHBOARD_API_URL || 'https://dashboard.aura.brightid.org'
+
+const $http = axios.create({
+ baseURL: DASHBOARD_API_BASE,
+ headers: {
+ "Cache-Control": "no-cache",
+ "Content-Type": "application/json"
+ },
+ timeout: 60 * 1000,
+})
+
+export const dashboardApi = {
+ // Project endpoints
+ getProjectsList: async () => {
+ return $http.get('/api/projects/list')
+ },
+
+ updateProject: async (projectId: string, projectData: any) => {
+ return $http.post(`/api/projects/update-project`, {
+ projectId,
+ ...projectData
+ })
+ },
+
+ createProject: async (projectData: any) => {
+ return $http.post('/api/projects/create-project', projectData)
+ },
+
+ upgradeProject: async (projectId: string, planId: string) => {
+ return $http.post(`/api/projects/upgrade-project`, {
+ projectId,
+ planId
+ })
+ },
+
+ updateProjectBrightid: async (projectId: string, brightId: string) => {
+ return $http.post(`/api/projects/update-project-brightid`, {
+ projectId,
+ brightId
+ })
+ },
+
+ // Payment endpoints
+ getPaymentsHistory: async () => {
+ return $http.get('/api/payments/history')
+ },
+
+ getPaymentStatus: async (orderId: string) => {
+ return $http.get(`/api/payments/status/${orderId}`)
+ },
+
+ createInvoice: async (invoiceData: any) => {
+ return $http.post('/api/payments/create-invoice', invoiceData)
+ },
+
+ createPayment: async (paymentData: any) => {
+ return $http.post('/api/payments/create-payment', paymentData)
+ }
+}
\ No newline at end of file
diff --git a/packages/sdk/src/apis/index.ts b/packages/sdk/src/apis/index.ts
new file mode 100644
index 0000000..8f2bb30
--- /dev/null
+++ b/packages/sdk/src/apis/index.ts
@@ -0,0 +1,5 @@
+export * from "./brightid"
+export * from "./dashboard"
+export * from "./get-verified"
+export * from "./node"
+export * from "./recovery"
diff --git a/packages/sdk/src/utils/crypto.ts b/packages/sdk/src/utils/crypto.ts
index aed036e..9806fe9 100644
--- a/packages/sdk/src/utils/crypto.ts
+++ b/packages/sdk/src/utils/crypto.ts
@@ -1,55 +1,24 @@
-import { fromByteArray } from "base64-js"
-import CryptoJS from "crypto-js"
-import nacl from "tweetnacl"
import { type BrightIdBackup } from "types"
-
-export function encryptData(data: string, password: string) {
- return CryptoJS.AES.encrypt(data, password).toString()
-}
+import { decryptData, encryptData } from "@aura/domain/crypto"
+
+// Shared crypto primitives live in @aura/domain — re-exported here to keep the
+// sdk's public API unchanged.
+export {
+ b64ToUrlSafeB64,
+ decryptData,
+ encryptData,
+ generateB64Keypair,
+ hash,
+ randomWordArray,
+ uInt8ArrayToB64,
+ urlSafeRandomKey,
+ wordArrayToB64,
+} from "@aura/domain/crypto"
export function encryptUserData(userData: BrightIdBackup, password: string) {
return encryptData(JSON.stringify(userData), password)
}
-export function decryptData(data: string, password: string) {
- return CryptoJS.AES.decrypt(data, password).toString(CryptoJS.enc.Utf8)
-}
-
export function decryptUserData(encryptedUserData: string, password: string) {
return JSON.parse(decryptData(encryptedUserData, password))
}
-
-export const b64ToUrlSafeB64 = (s: string) => {
- const alts: {
- [key: string]: string
- } = {
- "/": "_",
- "+": "-",
- "=": "",
- }
- return s.replace(/[/+=]/g, (c) => alts[c]!)
-}
-
-export const hash = (data: string) => {
- const h = CryptoJS.SHA256(data)
- const b = h.toString(CryptoJS.enc.Base64)
- return b64ToUrlSafeB64(b)
-}
-
-export const randomWordArray = (size: number) =>
- CryptoJS.lib.WordArray.random(size)
-
-export const wordArrayToB64 = (WordArray: CryptoJS.lib.WordArray) =>
- CryptoJS.enc.Base64.stringify(WordArray)
-
-export const generateB64Keypair = () => {
- const { publicKey, secretKey } = nacl.sign.keyPair()
- const b64PublicKey = fromByteArray(publicKey)
-
- const b64SecretKey = fromByteArray(secretKey)
-
- return {
- privateKey: b64SecretKey,
- publicKey: b64PublicKey,
- }
-}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index bbe71ff..ded7048 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "@aura/ui",
"private": true,
- "version": "0.0.1",
+ "version": "0.0.2",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/packages/ui/src/components/button.ts b/packages/ui/src/components/button.ts
index e332e31..fcd2e41 100644
--- a/packages/ui/src/components/button.ts
+++ b/packages/ui/src/components/button.ts
@@ -1,7 +1,7 @@
import { css, html, LitElement } from "lit"
import { customElement, property } from "lit/decorators.js"
-export type ButtonVariant = "default" | "secondary" | "ghost" | "outline"
+export type ButtonVariant = "default" | "secondary" | "ghost" | "outline" | "glass"
export type ButtonSize = "sm" | "md" | "lg" | "icon" | "icon-sm" | "icon-lg"
export type ButtonColors =
| "primary"
@@ -27,6 +27,13 @@ export class ButtonElement extends LitElement {
@property({ type: Boolean, reflect: true })
disabled: boolean = false
+ /**
+ * Toggle/pill state: a selected button renders filled in its palette color
+ * regardless of variant, so call sites don't juggle variant/color pairs.
+ */
+ @property({ type: Boolean, reflect: true })
+ selected: boolean = false
+
@property({})
class: string | undefined
@@ -199,11 +206,49 @@ export class ButtonElement extends LitElement {
--bg: color-mix(in oklch, var(--color) 10%, transparent);
}
- /* Optional: stronger hover for outline */
- /* :host([variant="outline"]) button:hover:not(:disabled) {
- --bg: color-mix(in oklch, var(--color) 15%, transparent);
- --border: oklch(from var(--color) calc(l - 0.05) c h);
- } */
+ /* Glass — frosted translucent surface, mirrors a-card[variant="glass"] */
+ :host([variant="glass"]) button {
+ --bg: transparent;
+ /* blend toward the theme foreground so any palette stays readable */
+ --fg: color-mix(in oklch, var(--color) 45%, var(--foreground));
+ --border: color-mix(in oklch, var(--color) 20%, transparent);
+ background: linear-gradient(
+ 135deg,
+ color-mix(in oklch, var(--color) 18%, transparent) 0%,
+ color-mix(in oklch, var(--color) 8%, transparent) 100%
+ );
+ backdrop-filter: blur(6px) saturate(180%);
+ -webkit-backdrop-filter: blur(6px) saturate(180%);
+ box-shadow:
+ 0 1px 2px oklch(0 0 0 / 0.05),
+ 0 8px 24px oklch(0 0 0 / 0.1);
+ }
+
+ :host([variant="glass"]) button:hover:not(:disabled) {
+ background: linear-gradient(
+ 135deg,
+ color-mix(in oklch, var(--color) 28%, transparent) 0%,
+ color-mix(in oklch, var(--color) 14%, transparent) 100%
+ );
+ box-shadow:
+ 0 2px 4px oklch(0 0 0 / 0.06),
+ 0 12px 32px oklch(0 0 0 / 0.16);
+ }
+
+ /* Selected (toggle) — filled, wins over any variant. Uses the primary
+ palette so a neutral pill group lights up consistently when chosen. */
+ :host([selected]) button {
+ --bg: var(--primary);
+ --fg: var(--primary-foreground);
+ --border: transparent;
+ background: var(--primary);
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
+
+ :host([selected]) button:hover:not(:disabled) {
+ background: oklch(from var(--primary) calc(l + 0.05) c h);
+ }
`
protected render() {
diff --git a/packages/ui/src/components/card.ts b/packages/ui/src/components/card.ts
index d873625..a5df58e 100644
--- a/packages/ui/src/components/card.ts
+++ b/packages/ui/src/components/card.ts
@@ -3,8 +3,13 @@ import { customElement, property } from "lit/decorators.js"
@customElement("a-card")
export class CardElement extends LitElement {
+ /** Glass is the design default; use `variant="default"` for a solid card. */
@property({ reflect: true })
- variant: "default" | "glass" = "default"
+ variant: "default" | "glass" = "glass"
+
+ /** Clickable card: pointer cursor, hover lift, press feedback. */
+ @property({ type: Boolean, reflect: true })
+ interactive: boolean = false
static styles: CSSResultGroup = css`
:host {
@@ -29,11 +34,56 @@ export class CardElement extends LitElement {
/* Glass variant */
:host([variant="glass"]) {
- /* --card-bg: oklch(1 0 0 / 0.2); */
- --card-border: var(--border);
- backdrop-filter: blur(1px);
- -webkit-backdrop-filter: blur(1px);
- box-shadow: none;
+ --blur: 2px;
+ background: linear-gradient(
+ 135deg,
+ color-mix(in oklch, var(--card) 20%, transparent) 0%,
+ color-mix(in oklch, var(--card) 8%, transparent) 100%
+ );
+ border: none;
+ backdrop-filter: blur(var(--blur)) saturate(180%);
+ -webkit-backdrop-filter: blur(var(--blur)) saturate(180%);
+ box-shadow:
+ 0 1px 2px oklch(0 0 0 / 0.04),
+ 0 12px 40px oklch(0 0 0 / 0.12);
+ }
+
+ :host([variant="glass"]):hover {
+ box-shadow:
+ 0 2px 4px oklch(0 0 0 / 0.06),
+ 0 16px 50px oklch(0 0 0 / 0.18);
+ }
+
+ /* Interactive cards behave like buttons */
+ :host([interactive]) {
+ cursor: pointer;
+ transition:
+ background 0.2s ease,
+ border-color 0.2s ease,
+ box-shadow 0.2s ease,
+ transform 0.15s ease;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ :host([interactive][variant="default"]:hover) {
+ background: color-mix(in oklch, var(--foreground) 6%, var(--card));
+ }
+
+ :host([interactive][variant="glass"]:hover) {
+ background: linear-gradient(
+ 135deg,
+ color-mix(in oklch, var(--card) 30%, transparent) 0%,
+ color-mix(in oklch, var(--card) 14%, transparent) 100%
+ );
+ }
+
+ :host([interactive]:active) {
+ transform: scale(0.99);
+ }
+
+ :host([interactive]:focus-visible) {
+ outline: 2px solid var(--ring);
+ outline-offset: 2px;
}
`
diff --git a/packages/ui/src/components/dialog.ts b/packages/ui/src/components/dialog.ts
index 8f7d458..5a4dfec 100644
--- a/packages/ui/src/components/dialog.ts
+++ b/packages/ui/src/components/dialog.ts
@@ -6,6 +6,7 @@ export class DialogElement extends LitElement {
@property({ type: Boolean }) open = false
@state() private _animatingOut = false
+ private _hideTimer?: ReturnType
static styles = css`
:host {
@@ -50,7 +51,7 @@ export class DialogElement extends LitElement {
@@ -67,6 +68,12 @@ export class DialogElement extends LitElement {
show() {
if (this.open && !this._animatingOut) return
+ // Reopening mid-leave: cancel the pending close so `after-hide` (and the
+ // consumer's state cleanup) never fires against a now-open dialog.
+ if (this._hideTimer) {
+ clearTimeout(this._hideTimer)
+ this._hideTimer = undefined
+ }
this.open = true
this._animatingOut = false
@@ -82,6 +89,10 @@ export class DialogElement extends LitElement {
hide() {
if (!this.open || this._animatingOut) return
this._animatingOut = true
+ // Drop `.visible` now so the fade/scale-out transition actually plays over
+ // the next 220ms. Contents stay mounted (consumers keep their state until
+ // `after-hide`), so the exit animates the real content, not an empty shell.
+ this.open = false
this.dispatchEvent(
new CustomEvent("open-change", {
@@ -91,10 +102,15 @@ export class DialogElement extends LitElement {
}),
)
- // Wait for exit animation before fully closing
- setTimeout(() => {
- this.open = false
+ // Fire once the exit transition has finished and the dialog is fully
+ // transparent — only then may consumers unmount / clear their contents.
+ this._hideTimer = setTimeout(() => {
this._animatingOut = false
+ this._hideTimer = undefined
+
+ this.dispatchEvent(
+ new CustomEvent("after-hide", { bubbles: true, composed: true }),
+ )
}, 220)
}
diff --git a/packages/ui/src/components/head.ts b/packages/ui/src/components/head.ts
index a286f77..9cade9b 100644
--- a/packages/ui/src/components/head.ts
+++ b/packages/ui/src/components/head.ts
@@ -7,12 +7,12 @@ export class HeadingElement extends LitElement {
level: "1" | "2" | "3" | "4" | "5" | "6" = "2"
static styles = css`
+ /* No default margins — pages lay headings out with flex/gap; opt into
+ spacing with utility classes instead of fighting baked-in margins. */
:host {
display: block;
color: var(--foreground);
font-weight: 600;
- margin-top: 1.5em;
- margin-bottom: 0.5em;
}
:host([level="1"]) {
diff --git a/packages/ui/src/components/tab.ts b/packages/ui/src/components/tab.ts
index 3d45cf8..ef4244e 100644
--- a/packages/ui/src/components/tab.ts
+++ b/packages/ui/src/components/tab.ts
@@ -49,9 +49,13 @@ export class TabsElement extends LitElement {
}
`
- constructor() {
- super()
- this.addEventListener("slotchange", () => this._initialize())
+ // Re-measure the indicator whenever the tab strip changes size (responsive
+ // layouts, tabs appearing/disappearing change every sibling's width).
+ private _resizeObserver = new ResizeObserver(() => this._updateIndicator())
+
+ disconnectedCallback() {
+ super.disconnectedCallback()
+ this._resizeObserver.disconnect()
}
private _initialize() {
@@ -64,6 +68,8 @@ export class TabsElement extends LitElement {
}
protected firstUpdated(_changedProperties: PropertyValues): void {
+ const container = this.renderRoot.querySelector(".tab-list")
+ if (container) this._resizeObserver.observe(container)
requestAnimationFrame(() => {
if (!this.value && this.tabs.length > 0) {
this.value = (this.tabs[0] as any).value || ""
@@ -92,14 +98,20 @@ export class TabsElement extends LitElement {
return
}
+ // Layout metrics, not getBoundingClientRect: rects are scaled while an
+ // ancestor animates with `transform: scale()` (e.g. a-dialog opening) and
+ // ResizeObserver never fires for transforms, so a scaled measurement
+ // would stick.
const container = this.renderRoot.querySelector(".tab-list") as HTMLElement
- const tabRect = activeTab.getBoundingClientRect()
- const containerRect = container.getBoundingClientRect()
-
- const left = tabRect.left - containerRect.left + container.scrollLeft
- const width = tabRect.width
+ const styles = getComputedStyle(container)
+ const gap = parseFloat(styles.columnGap) || 0
+ let left = parseFloat(styles.paddingLeft) || 0
+ for (const tab of this.tabs) {
+ if (tab === activeTab) break
+ left += tab.offsetWidth + gap
+ }
- indicator.style.width = `${width}px`
+ indicator.style.width = `${activeTab.offsetWidth}px`
indicator.style.transform = `translateX(${left}px)`
}
@@ -147,7 +159,9 @@ export class TabsElement extends LitElement {
return html`
diff --git a/packages/ui/src/components/text.ts b/packages/ui/src/components/text.ts
index a5c67a4..961bf1a 100644
--- a/packages/ui/src/components/text.ts
+++ b/packages/ui/src/components/text.ts
@@ -6,6 +6,9 @@ export class TextElement extends LitElement {
@property({ type: String, reflect: true })
variant: "title" | "lead" | "body" | "small" | "muted" = "body"
+ @property({ type: String, reflect: true })
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl"
+
static styles = css`
:host {
display: block;
@@ -40,6 +43,36 @@ export class TextElement extends LitElement {
line-height: 1.375rem;
color: var(--muted-foreground);
}
+
+ :host([size="xs"]) {
+ font-size: 0.75rem;
+ line-height: 1rem;
+ }
+
+ :host([size="sm"]) {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ }
+
+ :host([size="md"]) {
+ font-size: 1rem;
+ line-height: 1.5rem;
+ }
+
+ :host([size="lg"]) {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+ }
+
+ :host([size="xl"]) {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+ }
+
+ :host([size="2xl"]) {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ }
`
render() {
diff --git a/packages/widgets/src/verification/index.ts b/packages/widgets/src/verification/index.ts
index 72805e3..762782b 100644
--- a/packages/widgets/src/verification/index.ts
+++ b/packages/widgets/src/verification/index.ts
@@ -5,7 +5,7 @@ import { focusedProject } from '@/lib/projects'
import { projects } from '@/states/projects'
import { userBrightId } from '@/states/user'
import type { Project } from '@/types/projects'
-import { getProjects } from '@/utils/apis'
+import { getProjects, verifyProject, type VerificationSignature } from '@/utils/apis'
import { EvaluationCategory } from '@/utils/aura'
import { getLevelupProgress } from '@/utils/score'
import { getSubjectVerifications } from '@/utils/subject'
@@ -51,6 +51,7 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
@state() private step: Step = 'connect'
@state() private previousStep: Step = 'connect'
@state() private isLoadingVerification = false
+ @state() private isGeneratingSignature = false
@state() private verificationData: ProgressStepData | null = null
static styles: CSSResultGroup = css`
@@ -208,11 +209,40 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
this._goToStep('connect')
}
- private _handleContinue() {
+ private async _handleContinue() {
+ const brightId = userBrightId.get()
+ const data = this.verificationData
+ let signature: VerificationSignature | undefined
+
+ // Only verified users reach the success step, so generate the signature
+ // from the API before handing control back to the embedding app.
+ if (brightId) {
+ this.isGeneratingSignature = true
+ try {
+ const result = await verifyProject(this.projectId, {
+ userId: brightId,
+ client: focusedProject.get()?.name ?? 'aura-get-verified',
+ auraScore: data?.auraScore,
+ auraLevel: data?.auraLevel
+ })
+ signature = result?.signature
+ } catch (err) {
+ console.error('Failed to generate verification signature', err)
+ } finally {
+ this.isGeneratingSignature = false
+ }
+ }
+
window.parent.postMessage(
JSON.stringify({
type: 'verification-success',
- app: 'aura-get-verified'
+ app: 'aura-get-verified',
+ data: {
+ brightId,
+ signature,
+ auraLevel: data?.auraLevel,
+ auraScore: data?.auraScore
+ }
}),
'*'
)
@@ -273,6 +303,7 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
this._handleContinue()}
>
`
diff --git a/packages/widgets/src/verification/success-step.ts b/packages/widgets/src/verification/success-step.ts
index 2367e82..8be0110 100644
--- a/packages/widgets/src/verification/success-step.ts
+++ b/packages/widgets/src/verification/success-step.ts
@@ -6,6 +6,7 @@ import './level-badge'
export class VerificationSuccessElement extends LitElement {
@property() appName = ''
@property({ type: Number }) level = 1
+ @property({ type: Boolean }) loading = false
static styles: CSSResultGroup = css`
:host {
@@ -131,8 +132,12 @@ export class VerificationSuccessElement extends LitElement {
-
this._emit('continue')}>
- Continue to ${this.appName}
+ this._emit('continue')}
+ >
+ ${this.loading ? 'Generating signature…' : html`Continue to ${this.appName}`}