Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions infrastructure/control-panel/src/routes/+layout.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,12 +15,11 @@
{ label: 'Actions', href: '/actions' }
];

const isActive = (href: string) =>
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
</script>

<main class="flex min-h-screen bg-gray-50">
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
<h4 class="text-lg font-semibold text-black">Navigation</h4>
<nav class="mt-6 flex flex-col gap-2">
{#each navLinks as link}
Expand Down
15 changes: 8 additions & 7 deletions infrastructure/control-panel/src/routes/actions/+page.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,10 +67,11 @@
</script>

<section class="max-w-3xl space-y-6">
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
<h2 class="text-2xl font-semibold text-black">Actions</h2>
<p class="mt-2 text-black-700">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
<p class="text-black-700 mt-2">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
control panel.
</p>

<div class="mt-6 space-y-4">
Expand All@@ -87,16 +88,17 @@
Trigger DreamSync
</ButtonAction>
</div>
<p class="text-sm text-black-500">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
<p class="text-black-500 text-sm">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
to finish.
</p>
</div>
</div>
</section>

{#if toast}
<div
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
toast.variant === 'success'
? 'border-green-200 bg-green-100 text-green-800'
: 'border-danger-200 bg-danger-100 text-danger-500'
Expand All@@ -105,4 +107,3 @@
<p class="font-semibold">{toast.message}</p>
</div>
{/if}

1 change: 1 addition & 0 deletions infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
"import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
Expand Down
95 changes: 84 additions & 11 deletions infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
import type { Store } from "@tauri-apps/plugin-store";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
import * as jose from "jose";
import NotificationService from "../../services/NotificationService";
import type { KeyService } from "./key";
import type { UserController } from "./user";
Expand DownExpand Up@@ -122,22 +123,94 @@ export class VaultController {
},
});

const existingPublicKey = whoisResponse.data?.publicKey;
if (existingPublicKey) {
// Public key already exists, mark as saved
localStorage.setItem(`publicKeySaved_${eName}`, "true");
console.log(`Public key already exists for ${eName}`);
return;
}
// Get key binding certificates array from whois response
const keyBindingCertificates =
whoisResponse.data?.keyBindingCertificates;

// Get public key using the exact same logic as onboarding/verification flow
// KEY_ID is always "default", context depends on whether user is pre-verification
// Get current device's public key to check if it already exists
const KEY_ID = "default";

// Determine context: check if user is pre-verification (fake/demo user)
const isFake = await this.#userController.isFake;
const context = isFake ? "pre-verification" : "onboarding";

let currentPublicKey: string | undefined;
try {
currentPublicKey = await this.#keyService.getPublicKey(
KEY_ID,
context,
);
} catch (error) {
console.error(
"Failed to get current public key for comparison:",
error,
);
// Continue to sync anyway
}

// If we have certificates and current key, check if it already exists
if (
keyBindingCertificates &&
Array.isArray(keyBindingCertificates) &&
keyBindingCertificates.length > 0 &&
currentPublicKey
) {
try {
// Get registry JWKS for JWT verification
const registryUrl = PUBLIC_REGISTRY_URL;
if (registryUrl) {
const jwksUrl = new URL(
"/.well-known/jwks.json",
registryUrl,
).toString();
const jwksResponse = await axios.get(jwksUrl, {
timeout: 10000,
});
const JWKS = jose.createLocalJWKSet(jwksResponse.data);

// Extract public keys from certificates and check if current key exists
for (const jwt of keyBindingCertificates) {
try {
const { payload } = await jose.jwtVerify(
jwt,
JWKS,
);

// Verify ename matches
if (payload.ename !== eName) {
continue;
}

// Extract publicKey from JWT payload
const extractedPublicKey =
payload.publicKey as string;
if (extractedPublicKey === currentPublicKey) {
// Current device's key already exists, mark as saved
localStorage.setItem(
`publicKeySaved_${eName}`,
"true",
);
console.log(
`Public key already exists for ${eName}`,
);
return;
}
} catch (error) {
// JWT verification failed, try next certificate
console.warn(
"Failed to verify key binding certificate:",
error,
);
}
}
}
} catch (error) {
console.error(
"Error checking existing public keys:",
error,
);
// Continue to sync anyway
}
}

console.log("=".repeat(70));
console.log("🔄 [VaultController] syncPublicKey called");
console.log("=".repeat(70));
Expand Down
50 changes: 30 additions & 20 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -676,43 +676,53 @@ export class DbService {
}

/**
* Gets the public key for a given eName.
* Gets all public keys for a given eName.
* @param eName - The eName identifier
* @returns The public key string, or null if not found
* @returns Array of public key strings, or empty array if not found
*/
async getPublicKey(eName: string): Promise<string | null> {
async getPublicKeys(eName: string): Promise<string[]> {
if (!eName) {
throw new Error("eName is required for getting public key");
throw new Error("eName is required for getting public keys");
}

const result = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (!result.records[0]) {
return null;
return [];
}

return result.records[0].get("publicKey") || null;
const publicKeys = result.records[0].get("publicKeys");
// Handle null/undefined and ensure we return an array
if (!publicKeys || !Array.isArray(publicKeys)) {
return [];
}

return publicKeys;
}

/**
* Sets or updates the public key for a given eName.
* Adds a public key to the array for a given eName (appends, avoids duplicates).
* @param eName - The eName identifier
* @param publicKey - The public key to store
* @param publicKey - The public key to add
*/
async setPublicKey(eName: string, publicKey: string): Promise<void> {
async addPublicKey(eName: string, publicKey: string): Promise<void> {
if (!eName) {
throw new Error("eName is required for setting public key");
throw new Error("eName is required for adding public key");
}
if (!publicKey) {
throw new Error("publicKey is required");
}

// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
await this.runQueryInternal(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
ON CREATE SET u.publicKeys = []
WITH u
WHERE NOT $publicKey IN u.publicKeys
SET u.publicKeys = u.publicKeys + $publicKey`,
{ eName, publicKey },
);
}
Expand DownExpand Up@@ -803,26 +813,26 @@ export class DbService {
}
}

// Copy User node with public key if it exists
// Copy User node with public keys if it exists
try {
const userResult = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (userResult.records.length > 0) {
const publicKey = userResult.records[0].get("publicKey");
if (publicKey) {
const publicKeys = userResult.records[0].get("publicKeys");
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
console.log(
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
);
await targetDbService.runQuery(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
{ eName, publicKey },
SET u.publicKeys = $publicKeys`,
{ eName, publicKeys },
);
console.log(
`[MIGRATION] User node with public key copied successfully`,
`[MIGRATION] User node with public keys copied successfully`,
);
}
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
/**
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
*
* This migration converts the single publicKey property to an array of publicKeys
* to support multiple device installs per user.
*
* For existing users with publicKey: converts to [publicKey]
* For users without publicKey: initializes as []
*/

import { Driver } from "neo4j-driver";

export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
const session = driver.session();
try {
// First, convert existing publicKey (string) to publicKeys (array)
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
const result = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
SET u.publicKeys = [u.publicKey]
REMOVE u.publicKey
RETURN count(u) as converted`
);

const converted = result.records[0]?.get("converted") || 0;
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);

// For users without publicKey, initialize publicKeys as empty array
const result2 = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
SET u.publicKeys = []
RETURN count(u) as initialized`
);

const initialized = result2.records[0]?.get("initialized") || 0;
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);

console.log("Migration completed: publicKey -> publicKeys array");
} catch (error) {
console.error("Error migrating publicKey to publicKeys array:", error);
throw error;
} finally {
await session.close();
}
}

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions infrastructure/control-panel/src/routes/+layout.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,12 +15,11 @@
{ label: 'Actions', href: '/actions' }
];

const isActive = (href: string) =>
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
</script>

<main class="flex min-h-screen bg-gray-50">
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
<h4 class="text-lg font-semibold text-black">Navigation</h4>
<nav class="mt-6 flex flex-col gap-2">
{#each navLinks as link}
Expand Down
15 changes: 8 additions & 7 deletions infrastructure/control-panel/src/routes/actions/+page.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,10 +67,11 @@
</script>

<section class="max-w-3xl space-y-6">
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
<h2 class="text-2xl font-semibold text-black">Actions</h2>
<p class="mt-2 text-black-700">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
<p class="text-black-700 mt-2">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
control panel.
</p>

<div class="mt-6 space-y-4">
Expand All@@ -87,16 +88,17 @@
Trigger DreamSync
</ButtonAction>
</div>
<p class="text-sm text-black-500">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
<p class="text-black-500 text-sm">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
to finish.
</p>
</div>
</div>
</section>

{#if toast}
<div
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
toast.variant === 'success'
? 'border-green-200 bg-green-100 text-green-800'
: 'border-danger-200 bg-danger-100 text-danger-500'
Expand All@@ -105,4 +107,3 @@
<p class="font-semibold">{toast.message}</p>
</div>
{/if}

1 change: 1 addition & 0 deletions infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
"import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
Expand Down
95 changes: 84 additions & 11 deletions infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
import type { Store } from "@tauri-apps/plugin-store";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
import * as jose from "jose";
import NotificationService from "../../services/NotificationService";
import type { KeyService } from "./key";
import type { UserController } from "./user";
Expand DownExpand Up@@ -122,22 +123,94 @@ export class VaultController {
},
});

const existingPublicKey = whoisResponse.data?.publicKey;
if (existingPublicKey) {
// Public key already exists, mark as saved
localStorage.setItem(`publicKeySaved_${eName}`, "true");
console.log(`Public key already exists for ${eName}`);
return;
}
// Get key binding certificates array from whois response
const keyBindingCertificates =
whoisResponse.data?.keyBindingCertificates;

// Get public key using the exact same logic as onboarding/verification flow
// KEY_ID is always "default", context depends on whether user is pre-verification
// Get current device's public key to check if it already exists
const KEY_ID = "default";

// Determine context: check if user is pre-verification (fake/demo user)
const isFake = await this.#userController.isFake;
const context = isFake ? "pre-verification" : "onboarding";

let currentPublicKey: string | undefined;
try {
currentPublicKey = await this.#keyService.getPublicKey(
KEY_ID,
context,
);
} catch (error) {
console.error(
"Failed to get current public key for comparison:",
error,
);
// Continue to sync anyway
}

// If we have certificates and current key, check if it already exists
if (
keyBindingCertificates &&
Array.isArray(keyBindingCertificates) &&
keyBindingCertificates.length > 0 &&
currentPublicKey
) {
try {
// Get registry JWKS for JWT verification
const registryUrl = PUBLIC_REGISTRY_URL;
if (registryUrl) {
const jwksUrl = new URL(
"/.well-known/jwks.json",
registryUrl,
).toString();
const jwksResponse = await axios.get(jwksUrl, {
timeout: 10000,
});
const JWKS = jose.createLocalJWKSet(jwksResponse.data);

// Extract public keys from certificates and check if current key exists
for (const jwt of keyBindingCertificates) {
try {
const { payload } = await jose.jwtVerify(
jwt,
JWKS,
);

// Verify ename matches
if (payload.ename !== eName) {
continue;
}

// Extract publicKey from JWT payload
const extractedPublicKey =
payload.publicKey as string;
if (extractedPublicKey === currentPublicKey) {
// Current device's key already exists, mark as saved
localStorage.setItem(
`publicKeySaved_${eName}`,
"true",
);
console.log(
`Public key already exists for ${eName}`,
);
return;
}
} catch (error) {
// JWT verification failed, try next certificate
console.warn(
"Failed to verify key binding certificate:",
error,
);
}
}
}
} catch (error) {
console.error(
"Error checking existing public keys:",
error,
);
// Continue to sync anyway
}
}

console.log("=".repeat(70));
console.log("🔄 [VaultController] syncPublicKey called");
console.log("=".repeat(70));
Expand Down
50 changes: 30 additions & 20 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -676,43 +676,53 @@ export class DbService {
}

/**
* Gets the public key for a given eName.
* Gets all public keys for a given eName.
* @param eName - The eName identifier
* @returns The public key string, or null if not found
* @returns Array of public key strings, or empty array if not found
*/
async getPublicKey(eName: string): Promise<string | null> {
async getPublicKeys(eName: string): Promise<string[]> {
if (!eName) {
throw new Error("eName is required for getting public key");
throw new Error("eName is required for getting public keys");
}

const result = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (!result.records[0]) {
return null;
return [];
}

return result.records[0].get("publicKey") || null;
const publicKeys = result.records[0].get("publicKeys");
// Handle null/undefined and ensure we return an array
if (!publicKeys || !Array.isArray(publicKeys)) {
return [];
}

return publicKeys;
}

/**
* Sets or updates the public key for a given eName.
* Adds a public key to the array for a given eName (appends, avoids duplicates).
* @param eName - The eName identifier
* @param publicKey - The public key to store
* @param publicKey - The public key to add
*/
async setPublicKey(eName: string, publicKey: string): Promise<void> {
async addPublicKey(eName: string, publicKey: string): Promise<void> {
if (!eName) {
throw new Error("eName is required for setting public key");
throw new Error("eName is required for adding public key");
}
if (!publicKey) {
throw new Error("publicKey is required");
}

// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
await this.runQueryInternal(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
ON CREATE SET u.publicKeys = []
WITH u
WHERE NOT $publicKey IN u.publicKeys
SET u.publicKeys = u.publicKeys + $publicKey`,
{ eName, publicKey },
);
}
Expand DownExpand Up@@ -803,26 +813,26 @@ export class DbService {
}
}

// Copy User node with public key if it exists
// Copy User node with public keys if it exists
try {
const userResult = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (userResult.records.length > 0) {
const publicKey = userResult.records[0].get("publicKey");
if (publicKey) {
const publicKeys = userResult.records[0].get("publicKeys");
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
console.log(
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
);
await targetDbService.runQuery(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
{ eName, publicKey },
SET u.publicKeys = $publicKeys`,
{ eName, publicKeys },
);
console.log(
`[MIGRATION] User node with public key copied successfully`,
`[MIGRATION] User node with public keys copied successfully`,
);
}
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
/**
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
*
* This migration converts the single publicKey property to an array of publicKeys
* to support multiple device installs per user.
*
* For existing users with publicKey: converts to [publicKey]
* For users without publicKey: initializes as []
*/

import { Driver } from "neo4j-driver";

export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
const session = driver.session();
try {
// First, convert existing publicKey (string) to publicKeys (array)
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
const result = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
SET u.publicKeys = [u.publicKey]
REMOVE u.publicKey
RETURN count(u) as converted`
);

const converted = result.records[0]?.get("converted") || 0;
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);

// For users without publicKey, initialize publicKeys as empty array
const result2 = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
SET u.publicKeys = []
RETURN count(u) as initialized`
);

const initialized = result2.records[0]?.get("initialized") || 0;
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);

console.log("Migration completed: publicKey -> publicKeys array");
} catch (error) {
console.error("Error migrating publicKey to publicKeys array:", error);
throw error;
} finally {
await session.close();
}
}

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions infrastructure/control-panel/src/routes/+layout.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,12 +15,11 @@
{ label: 'Actions', href: '/actions' }
];

const isActive = (href: string) =>
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
</script>

<main class="flex min-h-screen bg-gray-50">
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
<h4 class="text-lg font-semibold text-black">Navigation</h4>
<nav class="mt-6 flex flex-col gap-2">
{#each navLinks as link}
Expand Down
15 changes: 8 additions & 7 deletions infrastructure/control-panel/src/routes/actions/+page.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,10 +67,11 @@
</script>

<section class="max-w-3xl space-y-6">
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
<h2 class="text-2xl font-semibold text-black">Actions</h2>
<p class="mt-2 text-black-700">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
<p class="text-black-700 mt-2">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
control panel.
</p>

<div class="mt-6 space-y-4">
Expand All@@ -87,16 +88,17 @@
Trigger DreamSync
</ButtonAction>
</div>
<p class="text-sm text-black-500">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
<p class="text-black-500 text-sm">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
to finish.
</p>
</div>
</div>
</section>

{#if toast}
<div
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
toast.variant === 'success'
? 'border-green-200 bg-green-100 text-green-800'
: 'border-danger-200 bg-danger-100 text-danger-500'
Expand All@@ -105,4 +107,3 @@
<p class="font-semibold">{toast.message}</p>
</div>
{/if}

1 change: 1 addition & 0 deletions infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
"import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
Expand Down
95 changes: 84 additions & 11 deletions infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
import type { Store } from "@tauri-apps/plugin-store";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
import * as jose from "jose";
import NotificationService from "../../services/NotificationService";
import type { KeyService } from "./key";
import type { UserController } from "./user";
Expand DownExpand Up@@ -122,22 +123,94 @@ export class VaultController {
},
});

const existingPublicKey = whoisResponse.data?.publicKey;
if (existingPublicKey) {
// Public key already exists, mark as saved
localStorage.setItem(`publicKeySaved_${eName}`, "true");
console.log(`Public key already exists for ${eName}`);
return;
}
// Get key binding certificates array from whois response
const keyBindingCertificates =
whoisResponse.data?.keyBindingCertificates;

// Get public key using the exact same logic as onboarding/verification flow
// KEY_ID is always "default", context depends on whether user is pre-verification
// Get current device's public key to check if it already exists
const KEY_ID = "default";

// Determine context: check if user is pre-verification (fake/demo user)
const isFake = await this.#userController.isFake;
const context = isFake ? "pre-verification" : "onboarding";

let currentPublicKey: string | undefined;
try {
currentPublicKey = await this.#keyService.getPublicKey(
KEY_ID,
context,
);
} catch (error) {
console.error(
"Failed to get current public key for comparison:",
error,
);
// Continue to sync anyway
}

// If we have certificates and current key, check if it already exists
if (
keyBindingCertificates &&
Array.isArray(keyBindingCertificates) &&
keyBindingCertificates.length > 0 &&
currentPublicKey
) {
try {
// Get registry JWKS for JWT verification
const registryUrl = PUBLIC_REGISTRY_URL;
if (registryUrl) {
const jwksUrl = new URL(
"/.well-known/jwks.json",
registryUrl,
).toString();
const jwksResponse = await axios.get(jwksUrl, {
timeout: 10000,
});
const JWKS = jose.createLocalJWKSet(jwksResponse.data);

// Extract public keys from certificates and check if current key exists
for (const jwt of keyBindingCertificates) {
try {
const { payload } = await jose.jwtVerify(
jwt,
JWKS,
);

// Verify ename matches
if (payload.ename !== eName) {
continue;
}

// Extract publicKey from JWT payload
const extractedPublicKey =
payload.publicKey as string;
if (extractedPublicKey === currentPublicKey) {
// Current device's key already exists, mark as saved
localStorage.setItem(
`publicKeySaved_${eName}`,
"true",
);
console.log(
`Public key already exists for ${eName}`,
);
return;
}
} catch (error) {
// JWT verification failed, try next certificate
console.warn(
"Failed to verify key binding certificate:",
error,
);
}
}
}
} catch (error) {
console.error(
"Error checking existing public keys:",
error,
);
// Continue to sync anyway
}
}

console.log("=".repeat(70));
console.log("🔄 [VaultController] syncPublicKey called");
console.log("=".repeat(70));
Expand Down
50 changes: 30 additions & 20 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -676,43 +676,53 @@ export class DbService {
}

/**
* Gets the public key for a given eName.
* Gets all public keys for a given eName.
* @param eName - The eName identifier
* @returns The public key string, or null if not found
* @returns Array of public key strings, or empty array if not found
*/
async getPublicKey(eName: string): Promise<string | null> {
async getPublicKeys(eName: string): Promise<string[]> {
if (!eName) {
throw new Error("eName is required for getting public key");
throw new Error("eName is required for getting public keys");
}

const result = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (!result.records[0]) {
return null;
return [];
}

return result.records[0].get("publicKey") || null;
const publicKeys = result.records[0].get("publicKeys");
// Handle null/undefined and ensure we return an array
if (!publicKeys || !Array.isArray(publicKeys)) {
return [];
}

return publicKeys;
}

/**
* Sets or updates the public key for a given eName.
* Adds a public key to the array for a given eName (appends, avoids duplicates).
* @param eName - The eName identifier
* @param publicKey - The public key to store
* @param publicKey - The public key to add
*/
async setPublicKey(eName: string, publicKey: string): Promise<void> {
async addPublicKey(eName: string, publicKey: string): Promise<void> {
if (!eName) {
throw new Error("eName is required for setting public key");
throw new Error("eName is required for adding public key");
}
if (!publicKey) {
throw new Error("publicKey is required");
}

// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
await this.runQueryInternal(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
ON CREATE SET u.publicKeys = []
WITH u
WHERE NOT $publicKey IN u.publicKeys
SET u.publicKeys = u.publicKeys + $publicKey`,
{ eName, publicKey },
);
}
Expand DownExpand Up@@ -803,26 +813,26 @@ export class DbService {
}
}

// Copy User node with public key if it exists
// Copy User node with public keys if it exists
try {
const userResult = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (userResult.records.length > 0) {
const publicKey = userResult.records[0].get("publicKey");
if (publicKey) {
const publicKeys = userResult.records[0].get("publicKeys");
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
console.log(
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
);
await targetDbService.runQuery(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
{ eName, publicKey },
SET u.publicKeys = $publicKeys`,
{ eName, publicKeys },
);
console.log(
`[MIGRATION] User node with public key copied successfully`,
`[MIGRATION] User node with public keys copied successfully`,
);
}
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
/**
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
*
* This migration converts the single publicKey property to an array of publicKeys
* to support multiple device installs per user.
*
* For existing users with publicKey: converts to [publicKey]
* For users without publicKey: initializes as []
*/

import { Driver } from "neo4j-driver";

export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
const session = driver.session();
try {
// First, convert existing publicKey (string) to publicKeys (array)
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
const result = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
SET u.publicKeys = [u.publicKey]
REMOVE u.publicKey
RETURN count(u) as converted`
);

const converted = result.records[0]?.get("converted") || 0;
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);

// For users without publicKey, initialize publicKeys as empty array
const result2 = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
SET u.publicKeys = []
RETURN count(u) as initialized`
);

const initialized = result2.records[0]?.get("initialized") || 0;
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);

console.log("Migration completed: publicKey -> publicKeys array");
} catch (error) {
console.error("Error migrating publicKey to publicKeys array:", error);
throw error;
} finally {
await session.close();
}
}

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions infrastructure/control-panel/src/routes/+layout.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,12 +15,11 @@
{ label: 'Actions', href: '/actions' }
];

const isActive = (href: string) =>
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
</script>

<main class="flex min-h-screen bg-gray-50">
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
<h4 class="text-lg font-semibold text-black">Navigation</h4>
<nav class="mt-6 flex flex-col gap-2">
{#each navLinks as link}
Expand Down
15 changes: 8 additions & 7 deletions infrastructure/control-panel/src/routes/actions/+page.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,10 +67,11 @@
</script>

<section class="max-w-3xl space-y-6">
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
<h2 class="text-2xl font-semibold text-black">Actions</h2>
<p class="mt-2 text-black-700">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
<p class="text-black-700 mt-2">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
control panel.
</p>

<div class="mt-6 space-y-4">
Expand All@@ -87,16 +88,17 @@
Trigger DreamSync
</ButtonAction>
</div>
<p class="text-sm text-black-500">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
<p class="text-black-500 text-sm">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
to finish.
</p>
</div>
</div>
</section>

{#if toast}
<div
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
toast.variant === 'success'
? 'border-green-200 bg-green-100 text-green-800'
: 'border-danger-200 bg-danger-100 text-danger-500'
Expand All@@ -105,4 +107,3 @@
<p class="font-semibold">{toast.message}</p>
</div>
{/if}

1 change: 1 addition & 0 deletions infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
"import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
Expand Down
95 changes: 84 additions & 11 deletions infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
import type { Store } from "@tauri-apps/plugin-store";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
import * as jose from "jose";
import NotificationService from "../../services/NotificationService";
import type { KeyService } from "./key";
import type { UserController } from "./user";
Expand DownExpand Up@@ -122,22 +123,94 @@ export class VaultController {
},
});

const existingPublicKey = whoisResponse.data?.publicKey;
if (existingPublicKey) {
// Public key already exists, mark as saved
localStorage.setItem(`publicKeySaved_${eName}`, "true");
console.log(`Public key already exists for ${eName}`);
return;
}
// Get key binding certificates array from whois response
const keyBindingCertificates =
whoisResponse.data?.keyBindingCertificates;

// Get public key using the exact same logic as onboarding/verification flow
// KEY_ID is always "default", context depends on whether user is pre-verification
// Get current device's public key to check if it already exists
const KEY_ID = "default";

// Determine context: check if user is pre-verification (fake/demo user)
const isFake = await this.#userController.isFake;
const context = isFake ? "pre-verification" : "onboarding";

let currentPublicKey: string | undefined;
try {
currentPublicKey = await this.#keyService.getPublicKey(
KEY_ID,
context,
);
} catch (error) {
console.error(
"Failed to get current public key for comparison:",
error,
);
// Continue to sync anyway
}

// If we have certificates and current key, check if it already exists
if (
keyBindingCertificates &&
Array.isArray(keyBindingCertificates) &&
keyBindingCertificates.length > 0 &&
currentPublicKey
) {
try {
// Get registry JWKS for JWT verification
const registryUrl = PUBLIC_REGISTRY_URL;
if (registryUrl) {
const jwksUrl = new URL(
"/.well-known/jwks.json",
registryUrl,
).toString();
const jwksResponse = await axios.get(jwksUrl, {
timeout: 10000,
});
const JWKS = jose.createLocalJWKSet(jwksResponse.data);

// Extract public keys from certificates and check if current key exists
for (const jwt of keyBindingCertificates) {
try {
const { payload } = await jose.jwtVerify(
jwt,
JWKS,
);

// Verify ename matches
if (payload.ename !== eName) {
continue;
}

// Extract publicKey from JWT payload
const extractedPublicKey =
payload.publicKey as string;
if (extractedPublicKey === currentPublicKey) {
// Current device's key already exists, mark as saved
localStorage.setItem(
`publicKeySaved_${eName}`,
"true",
);
console.log(
`Public key already exists for ${eName}`,
);
return;
}
} catch (error) {
// JWT verification failed, try next certificate
console.warn(
"Failed to verify key binding certificate:",
error,
);
}
}
}
} catch (error) {
console.error(
"Error checking existing public keys:",
error,
);
// Continue to sync anyway
}
}

console.log("=".repeat(70));
console.log("🔄 [VaultController] syncPublicKey called");
console.log("=".repeat(70));
Expand Down
50 changes: 30 additions & 20 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -676,43 +676,53 @@ export class DbService {
}

/**
* Gets the public key for a given eName.
* Gets all public keys for a given eName.
* @param eName - The eName identifier
* @returns The public key string, or null if not found
* @returns Array of public key strings, or empty array if not found
*/
async getPublicKey(eName: string): Promise<string | null> {
async getPublicKeys(eName: string): Promise<string[]> {
if (!eName) {
throw new Error("eName is required for getting public key");
throw new Error("eName is required for getting public keys");
}

const result = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (!result.records[0]) {
return null;
return [];
}

return result.records[0].get("publicKey") || null;
const publicKeys = result.records[0].get("publicKeys");
// Handle null/undefined and ensure we return an array
if (!publicKeys || !Array.isArray(publicKeys)) {
return [];
}

return publicKeys;
}

/**
* Sets or updates the public key for a given eName.
* Adds a public key to the array for a given eName (appends, avoids duplicates).
* @param eName - The eName identifier
* @param publicKey - The public key to store
* @param publicKey - The public key to add
*/
async setPublicKey(eName: string, publicKey: string): Promise<void> {
async addPublicKey(eName: string, publicKey: string): Promise<void> {
if (!eName) {
throw new Error("eName is required for setting public key");
throw new Error("eName is required for adding public key");
}
if (!publicKey) {
throw new Error("publicKey is required");
}

// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
await this.runQueryInternal(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
ON CREATE SET u.publicKeys = []
WITH u
WHERE NOT $publicKey IN u.publicKeys
SET u.publicKeys = u.publicKeys + $publicKey`,
{ eName, publicKey },
);
}
Expand DownExpand Up@@ -803,26 +813,26 @@ export class DbService {
}
}

// Copy User node with public key if it exists
// Copy User node with public keys if it exists
try {
const userResult = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (userResult.records.length > 0) {
const publicKey = userResult.records[0].get("publicKey");
if (publicKey) {
const publicKeys = userResult.records[0].get("publicKeys");
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
console.log(
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
);
await targetDbService.runQuery(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
{ eName, publicKey },
SET u.publicKeys = $publicKeys`,
{ eName, publicKeys },
);
console.log(
`[MIGRATION] User node with public key copied successfully`,
`[MIGRATION] User node with public keys copied successfully`,
);
}
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
/**
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
*
* This migration converts the single publicKey property to an array of publicKeys
* to support multiple device installs per user.
*
* For existing users with publicKey: converts to [publicKey]
* For users without publicKey: initializes as []
*/

import { Driver } from "neo4j-driver";

export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
const session = driver.session();
try {
// First, convert existing publicKey (string) to publicKeys (array)
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
const result = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
SET u.publicKeys = [u.publicKey]
REMOVE u.publicKey
RETURN count(u) as converted`
);

const converted = result.records[0]?.get("converted") || 0;
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);

// For users without publicKey, initialize publicKeys as empty array
const result2 = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
SET u.publicKeys = []
RETURN count(u) as initialized`
);

const initialized = result2.records[0]?.get("initialized") || 0;
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);

console.log("Migration completed: publicKey -> publicKeys array");
} catch (error) {
console.error("Error migrating publicKey to publicKeys array:", error);
throw error;
} finally {
await session.close();
}
}

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions infrastructure/control-panel/src/routes/+layout.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,12 +15,11 @@
{ label: 'Actions', href: '/actions' }
];

const isActive = (href: string) =>
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
</script>

<main class="flex min-h-screen bg-gray-50">
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
<h4 class="text-lg font-semibold text-black">Navigation</h4>
<nav class="mt-6 flex flex-col gap-2">
{#each navLinks as link}
Expand Down
15 changes: 8 additions & 7 deletions infrastructure/control-panel/src/routes/actions/+page.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,10 +67,11 @@
</script>

<section class="max-w-3xl space-y-6">
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
<h2 class="text-2xl font-semibold text-black">Actions</h2>
<p class="mt-2 text-black-700">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
<p class="text-black-700 mt-2">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
control panel.
</p>

<div class="mt-6 space-y-4">
Expand All@@ -87,16 +88,17 @@
Trigger DreamSync
</ButtonAction>
</div>
<p class="text-sm text-black-500">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
<p class="text-black-500 text-sm">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
to finish.
</p>
</div>
</div>
</section>

{#if toast}
<div
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
toast.variant === 'success'
? 'border-green-200 bg-green-100 text-green-800'
: 'border-danger-200 bg-danger-100 text-danger-500'
Expand All@@ -105,4 +107,3 @@
<p class="font-semibold">{toast.message}</p>
</div>
{/if}

1 change: 1 addition & 0 deletions infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
"import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
Expand Down
95 changes: 84 additions & 11 deletions infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
import type { Store } from "@tauri-apps/plugin-store";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
import * as jose from "jose";
import NotificationService from "../../services/NotificationService";
import type { KeyService } from "./key";
import type { UserController } from "./user";
Expand DownExpand Up@@ -122,22 +123,94 @@ export class VaultController {
},
});

const existingPublicKey = whoisResponse.data?.publicKey;
if (existingPublicKey) {
// Public key already exists, mark as saved
localStorage.setItem(`publicKeySaved_${eName}`, "true");
console.log(`Public key already exists for ${eName}`);
return;
}
// Get key binding certificates array from whois response
const keyBindingCertificates =
whoisResponse.data?.keyBindingCertificates;

// Get public key using the exact same logic as onboarding/verification flow
// KEY_ID is always "default", context depends on whether user is pre-verification
// Get current device's public key to check if it already exists
const KEY_ID = "default";

// Determine context: check if user is pre-verification (fake/demo user)
const isFake = await this.#userController.isFake;
const context = isFake ? "pre-verification" : "onboarding";

let currentPublicKey: string | undefined;
try {
currentPublicKey = await this.#keyService.getPublicKey(
KEY_ID,
context,
);
} catch (error) {
console.error(
"Failed to get current public key for comparison:",
error,
);
// Continue to sync anyway
}

// If we have certificates and current key, check if it already exists
if (
keyBindingCertificates &&
Array.isArray(keyBindingCertificates) &&
keyBindingCertificates.length > 0 &&
currentPublicKey
) {
try {
// Get registry JWKS for JWT verification
const registryUrl = PUBLIC_REGISTRY_URL;
if (registryUrl) {
const jwksUrl = new URL(
"/.well-known/jwks.json",
registryUrl,
).toString();
const jwksResponse = await axios.get(jwksUrl, {
timeout: 10000,
});
const JWKS = jose.createLocalJWKSet(jwksResponse.data);

// Extract public keys from certificates and check if current key exists
for (const jwt of keyBindingCertificates) {
try {
const { payload } = await jose.jwtVerify(
jwt,
JWKS,
);

// Verify ename matches
if (payload.ename !== eName) {
continue;
}

// Extract publicKey from JWT payload
const extractedPublicKey =
payload.publicKey as string;
if (extractedPublicKey === currentPublicKey) {
// Current device's key already exists, mark as saved
localStorage.setItem(
`publicKeySaved_${eName}`,
"true",
);
console.log(
`Public key already exists for ${eName}`,
);
return;
}
} catch (error) {
// JWT verification failed, try next certificate
console.warn(
"Failed to verify key binding certificate:",
error,
);
}
}
}
} catch (error) {
console.error(
"Error checking existing public keys:",
error,
);
// Continue to sync anyway
}
}

console.log("=".repeat(70));
console.log("🔄 [VaultController] syncPublicKey called");
console.log("=".repeat(70));
Expand Down
50 changes: 30 additions & 20 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -676,43 +676,53 @@ export class DbService {
}

/**
* Gets the public key for a given eName.
* Gets all public keys for a given eName.
* @param eName - The eName identifier
* @returns The public key string, or null if not found
* @returns Array of public key strings, or empty array if not found
*/
async getPublicKey(eName: string): Promise<string | null> {
async getPublicKeys(eName: string): Promise<string[]> {
if (!eName) {
throw new Error("eName is required for getting public key");
throw new Error("eName is required for getting public keys");
}

const result = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (!result.records[0]) {
return null;
return [];
}

return result.records[0].get("publicKey") || null;
const publicKeys = result.records[0].get("publicKeys");
// Handle null/undefined and ensure we return an array
if (!publicKeys || !Array.isArray(publicKeys)) {
return [];
}

return publicKeys;
}

/**
* Sets or updates the public key for a given eName.
* Adds a public key to the array for a given eName (appends, avoids duplicates).
* @param eName - The eName identifier
* @param publicKey - The public key to store
* @param publicKey - The public key to add
*/
async setPublicKey(eName: string, publicKey: string): Promise<void> {
async addPublicKey(eName: string, publicKey: string): Promise<void> {
if (!eName) {
throw new Error("eName is required for setting public key");
throw new Error("eName is required for adding public key");
}
if (!publicKey) {
throw new Error("publicKey is required");
}

// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
await this.runQueryInternal(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
ON CREATE SET u.publicKeys = []
WITH u
WHERE NOT $publicKey IN u.publicKeys
SET u.publicKeys = u.publicKeys + $publicKey`,
{ eName, publicKey },
);
}
Expand DownExpand Up@@ -803,26 +813,26 @@ export class DbService {
}
}

// Copy User node with public key if it exists
// Copy User node with public keys if it exists
try {
const userResult = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (userResult.records.length > 0) {
const publicKey = userResult.records[0].get("publicKey");
if (publicKey) {
const publicKeys = userResult.records[0].get("publicKeys");
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
console.log(
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
);
await targetDbService.runQuery(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
{ eName, publicKey },
SET u.publicKeys = $publicKeys`,
{ eName, publicKeys },
);
console.log(
`[MIGRATION] User node with public key copied successfully`,
`[MIGRATION] User node with public keys copied successfully`,
);
}
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
/**
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
*
* This migration converts the single publicKey property to an array of publicKeys
* to support multiple device installs per user.
*
* For existing users with publicKey: converts to [publicKey]
* For users without publicKey: initializes as []
*/

import { Driver } from "neo4j-driver";

export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
const session = driver.session();
try {
// First, convert existing publicKey (string) to publicKeys (array)
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
const result = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
SET u.publicKeys = [u.publicKey]
REMOVE u.publicKey
RETURN count(u) as converted`
);

const converted = result.records[0]?.get("converted") || 0;
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);

// For users without publicKey, initialize publicKeys as empty array
const result2 = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
SET u.publicKeys = []
RETURN count(u) as initialized`
);

const initialized = result2.records[0]?.get("initialized") || 0;
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);

console.log("Migration completed: publicKey -> publicKeys array");
} catch (error) {
console.error("Error migrating publicKey to publicKeys array:", error);
throw error;
} finally {
await session.close();
}
}

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions infrastructure/control-panel/src/routes/+layout.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,12 +15,11 @@
{ label: 'Actions', href: '/actions' }
];

const isActive = (href: string) =>
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
</script>

<main class="flex min-h-screen bg-gray-50">
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
<h4 class="text-lg font-semibold text-black">Navigation</h4>
<nav class="mt-6 flex flex-col gap-2">
{#each navLinks as link}
Expand Down
15 changes: 8 additions & 7 deletions infrastructure/control-panel/src/routes/actions/+page.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,10 +67,11 @@
</script>

<section class="max-w-3xl space-y-6">
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
<h2 class="text-2xl font-semibold text-black">Actions</h2>
<p class="mt-2 text-black-700">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
<p class="text-black-700 mt-2">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
control panel.
</p>

<div class="mt-6 space-y-4">
Expand All@@ -87,16 +88,17 @@
Trigger DreamSync
</ButtonAction>
</div>
<p class="text-sm text-black-500">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
<p class="text-black-500 text-sm">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
to finish.
</p>
</div>
</div>
</section>

{#if toast}
<div
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
toast.variant === 'success'
? 'border-green-200 bg-green-100 text-green-800'
: 'border-danger-200 bg-danger-100 text-danger-500'
Expand All@@ -105,4 +107,3 @@
<p class="font-semibold">{toast.message}</p>
</div>
{/if}

1 change: 1 addition & 0 deletions infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
"import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
Expand Down
95 changes: 84 additions & 11 deletions infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
import type { Store } from "@tauri-apps/plugin-store";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
import * as jose from "jose";
import NotificationService from "../../services/NotificationService";
import type { KeyService } from "./key";
import type { UserController } from "./user";
Expand DownExpand Up@@ -122,22 +123,94 @@ export class VaultController {
},
});

const existingPublicKey = whoisResponse.data?.publicKey;
if (existingPublicKey) {
// Public key already exists, mark as saved
localStorage.setItem(`publicKeySaved_${eName}`, "true");
console.log(`Public key already exists for ${eName}`);
return;
}
// Get key binding certificates array from whois response
const keyBindingCertificates =
whoisResponse.data?.keyBindingCertificates;

// Get public key using the exact same logic as onboarding/verification flow
// KEY_ID is always "default", context depends on whether user is pre-verification
// Get current device's public key to check if it already exists
const KEY_ID = "default";

// Determine context: check if user is pre-verification (fake/demo user)
const isFake = await this.#userController.isFake;
const context = isFake ? "pre-verification" : "onboarding";

let currentPublicKey: string | undefined;
try {
currentPublicKey = await this.#keyService.getPublicKey(
KEY_ID,
context,
);
} catch (error) {
console.error(
"Failed to get current public key for comparison:",
error,
);
// Continue to sync anyway
}

// If we have certificates and current key, check if it already exists
if (
keyBindingCertificates &&
Array.isArray(keyBindingCertificates) &&
keyBindingCertificates.length > 0 &&
currentPublicKey
) {
try {
// Get registry JWKS for JWT verification
const registryUrl = PUBLIC_REGISTRY_URL;
if (registryUrl) {
const jwksUrl = new URL(
"/.well-known/jwks.json",
registryUrl,
).toString();
const jwksResponse = await axios.get(jwksUrl, {
timeout: 10000,
});
const JWKS = jose.createLocalJWKSet(jwksResponse.data);

// Extract public keys from certificates and check if current key exists
for (const jwt of keyBindingCertificates) {
try {
const { payload } = await jose.jwtVerify(
jwt,
JWKS,
);

// Verify ename matches
if (payload.ename !== eName) {
continue;
}

// Extract publicKey from JWT payload
const extractedPublicKey =
payload.publicKey as string;
if (extractedPublicKey === currentPublicKey) {
// Current device's key already exists, mark as saved
localStorage.setItem(
`publicKeySaved_${eName}`,
"true",
);
console.log(
`Public key already exists for ${eName}`,
);
return;
}
} catch (error) {
// JWT verification failed, try next certificate
console.warn(
"Failed to verify key binding certificate:",
error,
);
}
}
}
} catch (error) {
console.error(
"Error checking existing public keys:",
error,
);
// Continue to sync anyway
}
}

console.log("=".repeat(70));
console.log("🔄 [VaultController] syncPublicKey called");
console.log("=".repeat(70));
Expand Down
50 changes: 30 additions & 20 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -676,43 +676,53 @@ export class DbService {
}

/**
* Gets the public key for a given eName.
* Gets all public keys for a given eName.
* @param eName - The eName identifier
* @returns The public key string, or null if not found
* @returns Array of public key strings, or empty array if not found
*/
async getPublicKey(eName: string): Promise<string | null> {
async getPublicKeys(eName: string): Promise<string[]> {
if (!eName) {
throw new Error("eName is required for getting public key");
throw new Error("eName is required for getting public keys");
}

const result = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (!result.records[0]) {
return null;
return [];
}

return result.records[0].get("publicKey") || null;
const publicKeys = result.records[0].get("publicKeys");
// Handle null/undefined and ensure we return an array
if (!publicKeys || !Array.isArray(publicKeys)) {
return [];
}

return publicKeys;
}

/**
* Sets or updates the public key for a given eName.
* Adds a public key to the array for a given eName (appends, avoids duplicates).
* @param eName - The eName identifier
* @param publicKey - The public key to store
* @param publicKey - The public key to add
*/
async setPublicKey(eName: string, publicKey: string): Promise<void> {
async addPublicKey(eName: string, publicKey: string): Promise<void> {
if (!eName) {
throw new Error("eName is required for setting public key");
throw new Error("eName is required for adding public key");
}
if (!publicKey) {
throw new Error("publicKey is required");
}

// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
await this.runQueryInternal(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
ON CREATE SET u.publicKeys = []
WITH u
WHERE NOT $publicKey IN u.publicKeys
SET u.publicKeys = u.publicKeys + $publicKey`,
{ eName, publicKey },
);
}
Expand DownExpand Up@@ -803,26 +813,26 @@ export class DbService {
}
}

// Copy User node with public key if it exists
// Copy User node with public keys if it exists
try {
const userResult = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (userResult.records.length > 0) {
const publicKey = userResult.records[0].get("publicKey");
if (publicKey) {
const publicKeys = userResult.records[0].get("publicKeys");
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
console.log(
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
);
await targetDbService.runQuery(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
{ eName, publicKey },
SET u.publicKeys = $publicKeys`,
{ eName, publicKeys },
);
console.log(
`[MIGRATION] User node with public key copied successfully`,
`[MIGRATION] User node with public keys copied successfully`,
);
}
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
/**
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
*
* This migration converts the single publicKey property to an array of publicKeys
* to support multiple device installs per user.
*
* For existing users with publicKey: converts to [publicKey]
* For users without publicKey: initializes as []
*/

import { Driver } from "neo4j-driver";

export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
const session = driver.session();
try {
// First, convert existing publicKey (string) to publicKeys (array)
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
const result = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
SET u.publicKeys = [u.publicKey]
REMOVE u.publicKey
RETURN count(u) as converted`
);

const converted = result.records[0]?.get("converted") || 0;
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);

// For users without publicKey, initialize publicKeys as empty array
const result2 = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
SET u.publicKeys = []
RETURN count(u) as initialized`
);

const initialized = result2.records[0]?.get("initialized") || 0;
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);

console.log("Migration completed: publicKey -> publicKeys array");
} catch (error) {
console.error("Error migrating publicKey to publicKeys array:", error);
throw error;
} finally {
await session.close();
}
}

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions infrastructure/control-panel/src/routes/+layout.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,12 +15,11 @@
{ label: 'Actions', href: '/actions' }
];

const isActive = (href: string) =>
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
</script>

<main class="flex min-h-screen bg-gray-50">
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
<h4 class="text-lg font-semibold text-black">Navigation</h4>
<nav class="mt-6 flex flex-col gap-2">
{#each navLinks as link}
Expand Down
15 changes: 8 additions & 7 deletions infrastructure/control-panel/src/routes/actions/+page.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,10 +67,11 @@
</script>

<section class="max-w-3xl space-y-6">
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
<h2 class="text-2xl font-semibold text-black">Actions</h2>
<p class="mt-2 text-black-700">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
<p class="text-black-700 mt-2">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
control panel.
</p>

<div class="mt-6 space-y-4">
Expand All@@ -87,16 +88,17 @@
Trigger DreamSync
</ButtonAction>
</div>
<p class="text-sm text-black-500">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
<p class="text-black-500 text-sm">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
to finish.
</p>
</div>
</div>
</section>

{#if toast}
<div
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
toast.variant === 'success'
? 'border-green-200 bg-green-100 text-green-800'
: 'border-danger-200 bg-danger-100 text-danger-500'
Expand All@@ -105,4 +107,3 @@
<p class="font-semibold">{toast.message}</p>
</div>
{/if}

1 change: 1 addition & 0 deletions infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
"import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
Expand Down
95 changes: 84 additions & 11 deletions infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
import type { Store } from "@tauri-apps/plugin-store";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
import * as jose from "jose";
import NotificationService from "../../services/NotificationService";
import type { KeyService } from "./key";
import type { UserController } from "./user";
Expand DownExpand Up@@ -122,22 +123,94 @@ export class VaultController {
},
});

const existingPublicKey = whoisResponse.data?.publicKey;
if (existingPublicKey) {
// Public key already exists, mark as saved
localStorage.setItem(`publicKeySaved_${eName}`, "true");
console.log(`Public key already exists for ${eName}`);
return;
}
// Get key binding certificates array from whois response
const keyBindingCertificates =
whoisResponse.data?.keyBindingCertificates;

// Get public key using the exact same logic as onboarding/verification flow
// KEY_ID is always "default", context depends on whether user is pre-verification
// Get current device's public key to check if it already exists
const KEY_ID = "default";

// Determine context: check if user is pre-verification (fake/demo user)
const isFake = await this.#userController.isFake;
const context = isFake ? "pre-verification" : "onboarding";

let currentPublicKey: string | undefined;
try {
currentPublicKey = await this.#keyService.getPublicKey(
KEY_ID,
context,
);
} catch (error) {
console.error(
"Failed to get current public key for comparison:",
error,
);
// Continue to sync anyway
}

// If we have certificates and current key, check if it already exists
if (
keyBindingCertificates &&
Array.isArray(keyBindingCertificates) &&
keyBindingCertificates.length > 0 &&
currentPublicKey
) {
try {
// Get registry JWKS for JWT verification
const registryUrl = PUBLIC_REGISTRY_URL;
if (registryUrl) {
const jwksUrl = new URL(
"/.well-known/jwks.json",
registryUrl,
).toString();
const jwksResponse = await axios.get(jwksUrl, {
timeout: 10000,
});
const JWKS = jose.createLocalJWKSet(jwksResponse.data);

// Extract public keys from certificates and check if current key exists
for (const jwt of keyBindingCertificates) {
try {
const { payload } = await jose.jwtVerify(
jwt,
JWKS,
);

// Verify ename matches
if (payload.ename !== eName) {
continue;
}

// Extract publicKey from JWT payload
const extractedPublicKey =
payload.publicKey as string;
if (extractedPublicKey === currentPublicKey) {
// Current device's key already exists, mark as saved
localStorage.setItem(
`publicKeySaved_${eName}`,
"true",
);
console.log(
`Public key already exists for ${eName}`,
);
return;
}
} catch (error) {
// JWT verification failed, try next certificate
console.warn(
"Failed to verify key binding certificate:",
error,
);
}
}
}
} catch (error) {
console.error(
"Error checking existing public keys:",
error,
);
// Continue to sync anyway
}
}

console.log("=".repeat(70));
console.log("🔄 [VaultController] syncPublicKey called");
console.log("=".repeat(70));
Expand Down
50 changes: 30 additions & 20 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -676,43 +676,53 @@ export class DbService {
}

/**
* Gets the public key for a given eName.
* Gets all public keys for a given eName.
* @param eName - The eName identifier
* @returns The public key string, or null if not found
* @returns Array of public key strings, or empty array if not found
*/
async getPublicKey(eName: string): Promise<string | null> {
async getPublicKeys(eName: string): Promise<string[]> {
if (!eName) {
throw new Error("eName is required for getting public key");
throw new Error("eName is required for getting public keys");
}

const result = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (!result.records[0]) {
return null;
return [];
}

return result.records[0].get("publicKey") || null;
const publicKeys = result.records[0].get("publicKeys");
// Handle null/undefined and ensure we return an array
if (!publicKeys || !Array.isArray(publicKeys)) {
return [];
}

return publicKeys;
}

/**
* Sets or updates the public key for a given eName.
* Adds a public key to the array for a given eName (appends, avoids duplicates).
* @param eName - The eName identifier
* @param publicKey - The public key to store
* @param publicKey - The public key to add
*/
async setPublicKey(eName: string, publicKey: string): Promise<void> {
async addPublicKey(eName: string, publicKey: string): Promise<void> {
if (!eName) {
throw new Error("eName is required for setting public key");
throw new Error("eName is required for adding public key");
}
if (!publicKey) {
throw new Error("publicKey is required");
}

// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
await this.runQueryInternal(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
ON CREATE SET u.publicKeys = []
WITH u
WHERE NOT $publicKey IN u.publicKeys
SET u.publicKeys = u.publicKeys + $publicKey`,
{ eName, publicKey },
);
}
Expand DownExpand Up@@ -803,26 +813,26 @@ export class DbService {
}
}

// Copy User node with public key if it exists
// Copy User node with public keys if it exists
try {
const userResult = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (userResult.records.length > 0) {
const publicKey = userResult.records[0].get("publicKey");
if (publicKey) {
const publicKeys = userResult.records[0].get("publicKeys");
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
console.log(
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
);
await targetDbService.runQuery(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
{ eName, publicKey },
SET u.publicKeys = $publicKeys`,
{ eName, publicKeys },
);
console.log(
`[MIGRATION] User node with public key copied successfully`,
`[MIGRATION] User node with public keys copied successfully`,
);
}
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
/**
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
*
* This migration converts the single publicKey property to an array of publicKeys
* to support multiple device installs per user.
*
* For existing users with publicKey: converts to [publicKey]
* For users without publicKey: initializes as []
*/

import { Driver } from "neo4j-driver";

export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
const session = driver.session();
try {
// First, convert existing publicKey (string) to publicKeys (array)
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
const result = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
SET u.publicKeys = [u.publicKey]
REMOVE u.publicKey
RETURN count(u) as converted`
);

const converted = result.records[0]?.get("converted") || 0;
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);

// For users without publicKey, initialize publicKeys as empty array
const result2 = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
SET u.publicKeys = []
RETURN count(u) as initialized`
);

const initialized = result2.records[0]?.get("initialized") || 0;
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);

console.log("Migration completed: publicKey -> publicKeys array");
} catch (error) {
console.error("Error migrating publicKey to publicKeys array:", error);
throw error;
} finally {
await session.close();
}
}

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions infrastructure/control-panel/src/routes/+layout.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,12 +15,11 @@
{ label: 'Actions', href: '/actions' }
];

const isActive = (href: string) =>
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
</script>

<main class="flex min-h-screen bg-gray-50">
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
<h4 class="text-lg font-semibold text-black">Navigation</h4>
<nav class="mt-6 flex flex-col gap-2">
{#each navLinks as link}
Expand Down
15 changes: 8 additions & 7 deletions infrastructure/control-panel/src/routes/actions/+page.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,10 +67,11 @@
</script>

<section class="max-w-3xl space-y-6">
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
<h2 class="text-2xl font-semibold text-black">Actions</h2>
<p class="mt-2 text-black-700">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
<p class="text-black-700 mt-2">
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
control panel.
</p>

<div class="mt-6 space-y-4">
Expand All@@ -87,16 +88,17 @@
Trigger DreamSync
</ButtonAction>
</div>
<p class="text-sm text-black-500">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
<p class="text-black-500 text-sm">
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
to finish.
</p>
</div>
</div>
</section>

{#if toast}
<div
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
toast.variant === 'success'
? 'border-green-200 bg-green-100 text-green-800'
: 'border-danger-200 bg-danger-100 text-danger-500'
Expand All@@ -105,4 +107,3 @@
<p class="font-semibold">{toast.message}</p>
</div>
{/if}

1 change: 1 addition & 0 deletions infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
"import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
Expand Down
95 changes: 84 additions & 11 deletions infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
import type { Store } from "@tauri-apps/plugin-store";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
import * as jose from "jose";
import NotificationService from "../../services/NotificationService";
import type { KeyService } from "./key";
import type { UserController } from "./user";
Expand DownExpand Up@@ -122,22 +123,94 @@ export class VaultController {
},
});

const existingPublicKey = whoisResponse.data?.publicKey;
if (existingPublicKey) {
// Public key already exists, mark as saved
localStorage.setItem(`publicKeySaved_${eName}`, "true");
console.log(`Public key already exists for ${eName}`);
return;
}
// Get key binding certificates array from whois response
const keyBindingCertificates =
whoisResponse.data?.keyBindingCertificates;

// Get public key using the exact same logic as onboarding/verification flow
// KEY_ID is always "default", context depends on whether user is pre-verification
// Get current device's public key to check if it already exists
const KEY_ID = "default";

// Determine context: check if user is pre-verification (fake/demo user)
const isFake = await this.#userController.isFake;
const context = isFake ? "pre-verification" : "onboarding";

let currentPublicKey: string | undefined;
try {
currentPublicKey = await this.#keyService.getPublicKey(
KEY_ID,
context,
);
} catch (error) {
console.error(
"Failed to get current public key for comparison:",
error,
);
// Continue to sync anyway
}

// If we have certificates and current key, check if it already exists
if (
keyBindingCertificates &&
Array.isArray(keyBindingCertificates) &&
keyBindingCertificates.length > 0 &&
currentPublicKey
) {
try {
// Get registry JWKS for JWT verification
const registryUrl = PUBLIC_REGISTRY_URL;
if (registryUrl) {
const jwksUrl = new URL(
"/.well-known/jwks.json",
registryUrl,
).toString();
const jwksResponse = await axios.get(jwksUrl, {
timeout: 10000,
});
const JWKS = jose.createLocalJWKSet(jwksResponse.data);

// Extract public keys from certificates and check if current key exists
for (const jwt of keyBindingCertificates) {
try {
const { payload } = await jose.jwtVerify(
jwt,
JWKS,
);

// Verify ename matches
if (payload.ename !== eName) {
continue;
}

// Extract publicKey from JWT payload
const extractedPublicKey =
payload.publicKey as string;
if (extractedPublicKey === currentPublicKey) {
// Current device's key already exists, mark as saved
localStorage.setItem(
`publicKeySaved_${eName}`,
"true",
);
console.log(
`Public key already exists for ${eName}`,
);
return;
}
} catch (error) {
// JWT verification failed, try next certificate
console.warn(
"Failed to verify key binding certificate:",
error,
);
}
}
}
} catch (error) {
console.error(
"Error checking existing public keys:",
error,
);
// Continue to sync anyway
}
}

console.log("=".repeat(70));
console.log("🔄 [VaultController] syncPublicKey called");
console.log("=".repeat(70));
Expand Down
50 changes: 30 additions & 20 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -676,43 +676,53 @@ export class DbService {
}

/**
* Gets the public key for a given eName.
* Gets all public keys for a given eName.
* @param eName - The eName identifier
* @returns The public key string, or null if not found
* @returns Array of public key strings, or empty array if not found
*/
async getPublicKey(eName: string): Promise<string | null> {
async getPublicKeys(eName: string): Promise<string[]> {
if (!eName) {
throw new Error("eName is required for getting public key");
throw new Error("eName is required for getting public keys");
}

const result = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (!result.records[0]) {
return null;
return [];
}

return result.records[0].get("publicKey") || null;
const publicKeys = result.records[0].get("publicKeys");
// Handle null/undefined and ensure we return an array
if (!publicKeys || !Array.isArray(publicKeys)) {
return [];
}

return publicKeys;
}

/**
* Sets or updates the public key for a given eName.
* Adds a public key to the array for a given eName (appends, avoids duplicates).
* @param eName - The eName identifier
* @param publicKey - The public key to store
* @param publicKey - The public key to add
*/
async setPublicKey(eName: string, publicKey: string): Promise<void> {
async addPublicKey(eName: string, publicKey: string): Promise<void> {
if (!eName) {
throw new Error("eName is required for setting public key");
throw new Error("eName is required for adding public key");
}
if (!publicKey) {
throw new Error("publicKey is required");
}

// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
await this.runQueryInternal(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
ON CREATE SET u.publicKeys = []
WITH u
WHERE NOT $publicKey IN u.publicKeys
SET u.publicKeys = u.publicKeys + $publicKey`,
{ eName, publicKey },
);
}
Expand DownExpand Up@@ -803,26 +813,26 @@ export class DbService {
}
}

// Copy User node with public key if it exists
// Copy User node with public keys if it exists
try {
const userResult = await this.runQueryInternal(
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
{ eName },
);

if (userResult.records.length > 0) {
const publicKey = userResult.records[0].get("publicKey");
if (publicKey) {
const publicKeys = userResult.records[0].get("publicKeys");
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
console.log(
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
);
await targetDbService.runQuery(
`MERGE (u:User { eName: $eName })
SET u.publicKey = $publicKey`,
{ eName, publicKey },
SET u.publicKeys = $publicKeys`,
{ eName, publicKeys },
);
console.log(
`[MIGRATION] User node with public key copied successfully`,
`[MIGRATION] User node with public keys copied successfully`,
);
}
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
/**
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
*
* This migration converts the single publicKey property to an array of publicKeys
* to support multiple device installs per user.
*
* For existing users with publicKey: converts to [publicKey]
* For users without publicKey: initializes as []
*/

import { Driver } from "neo4j-driver";

export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
const session = driver.session();
try {
// First, convert existing publicKey (string) to publicKeys (array)
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
const result = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
SET u.publicKeys = [u.publicKey]
REMOVE u.publicKey
RETURN count(u) as converted`
);

const converted = result.records[0]?.get("converted") || 0;
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);

// For users without publicKey, initialize publicKeys as empty array
const result2 = await session.run(
`MATCH (u:User)
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
SET u.publicKeys = []
RETURN count(u) as initialized`
);

const initialized = result2.records[0]?.get("initialized") || 0;
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);

console.log("Migration completed: publicKey -> publicKeys array");
} catch (error) {
console.error("Error migrating publicKey to publicKeys array:", error);
throw error;
} finally {
await session.close();
}
}

Loading