diff --git a/.github/workflows/vetkeys-password-manager-with-metadata.yml b/.github/workflows/vetkeys-password-manager-with-metadata.yml
new file mode 100644
index 000000000..95dfa5679
--- /dev/null
+++ b/.github/workflows/vetkeys-password-manager-with-metadata.yml
@@ -0,0 +1,36 @@
+name: vetkeys-password-manager-with-metadata
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ paths:
+ - rust/vetkeys/password_manager_with_metadata/**
+ - .github/workflows/vetkeys-password-manager-with-metadata.yml
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ rust:
+ runs-on: ubuntu-24.04
+ container: ghcr.io/dfinity/icp-dev-env-rust:0.1.0
+ env:
+ ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
+ - name: Deploy Password Manager With Metadata Rust
+ working-directory: rust/vetkeys/password_manager_with_metadata/rust
+ run: icp network start -d && icp deploy
+ motoko:
+ runs-on: ubuntu-24.04
+ container: ghcr.io/dfinity/icp-dev-env-motoko:0.1.0
+ env:
+ ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
+ - name: Deploy Password Manager With Metadata Motoko
+ working-directory: rust/vetkeys/password_manager_with_metadata/motoko
+ run: icp network start -d && icp deploy
diff --git a/rust/vetkeys/password_manager_with_metadata/README.md b/rust/vetkeys/password_manager_with_metadata/README.md
index 0f8d2e80f..719685c3a 100644
--- a/rust/vetkeys/password_manager_with_metadata/README.md
+++ b/rust/vetkeys/password_manager_with_metadata/README.md
@@ -1,8 +1,10 @@
# VetKey Password Manager with Metadata
+
The **VetKey Password Manager** is an example application demonstrating how to use **VetKeys** and **Encrypted Maps** to build a secure, decentralized password manager on the **Internet Computer (IC)**. This application allows users to create password vaults, store encrypted passwords, and share vaults with other users via their **Internet Identity Principal**.
@@ -19,39 +21,61 @@ This version of the application extends the basic password manager by supporting
### Prerequisites
-- [Local Internet Computer dev environment](https://internetcomputer.org/docs/building-apps/getting-started/install)
+- [ICP CLI](https://cli.internetcomputer.org)
- [npm](https://www.npmjs.com/package/npm)
### (Optionally) Choose a Different Master Key
-This example uses `test_key_1` by default. To use a different [available master key](https://internetcomputer.org/docs/building-apps/network-features/vetkeys/api#available-master-keys), change the `"init_arg": "(\"test_key_1\")"` line in `dfx.json` to the desired key before running `dfx deploy` in the next step.
+This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` to the desired key before running `icp deploy` in the next step.
+
+### Folder Structure
+
+This example provides both a **Rust** and a **Motoko** backend, sharing a common `frontend/`:
+
+```
+password_manager_with_metadata/
+├── frontend/ ← shared frontend (symlinked into rust/ and motoko/)
+├── motoko/ ← Motoko backend + icp.yaml
+└── rust/ ← Rust backend + icp.yaml
+```
### Deploy the Canisters Locally
-If you want to deploy this project locally with a Motoko backend, then run:
+Deploy with the **Motoko** backend:
```bash
-dfx start --background && dfx deploy
+cd motoko
+icp network start -d && icp deploy
```
-from the `motoko` folder.
-To use the Rust backend instead of Motoko, run the same command in the `rust` folder.
+Or deploy with the **Rust** backend:
+```bash
+cd rust
+icp network start -d && icp deploy
+```
-## Running the Project
+To run the frontend in development mode with hot reloading (after running `icp deploy`):
+```bash
+cd frontend
+npm run dev:motoko # if you deployed the Motoko backend
+# or
+npm run dev:rust # if you deployed the Rust backend
+```
+
+When you are done testing, stop the local network to free up resources and unblock the default port for other projects:
+```bash
+icp network stop
+```
+
+## Example Components
### Backend
-The backend consists of an **Encrypted Maps**-enabled canister that securely stores passwords. It is automatically deployed with `dfx deploy`.
+The backend consists of an **Encrypted Maps**-enabled canister that securely stores passwords.
### Frontend
The frontend is a **Svelte** application providing a user-friendly interface for managing vaults and passwords.
-To run the frontend in development mode with hot reloading:
-
-```bash
-npm run dev
-```
-
## Limitations
This example dapp does not implement key rotation, which is strongly recommended in a production environment.
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/package.json b/rust/vetkeys/password_manager_with_metadata/frontend/package.json
index 49dd1e0e1..a50afea07 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/package.json
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/package.json
@@ -5,7 +5,9 @@
"type": "module",
"scripts": {
"build": "npm run build:bindings && vite build",
- "dev": "npm run build:bindings && vite",
+ "dev": "printf '\\nNo backend specified. Use one of:\\n\\n npm run dev:motoko\\n npm run dev:rust\\n\\n' && exit 1",
+ "dev:motoko": "npm run build:bindings && BACKEND=motoko vite",
+ "dev:rust": "npm run build:bindings && BACKEND=rust vite",
"build:bindings": "cd scripts && ./gen_bindings.sh",
"lint": "eslint",
"prettier": "prettier --write .",
@@ -32,16 +34,12 @@
"typescript-eslint": "^8.26.1",
"vite": "^5.4.21",
"vite-plugin-compression": "^0.5.1",
- "vite-plugin-environment": "^1.1.3",
"vite-plugin-eslint": "^1.8.1"
},
"dependencies": {
- "@dfinity/agent": "^2.3.0",
- "@dfinity/auth-client": "^2.3.0",
- "@dfinity/candid": "^2.3.0",
- "@dfinity/identity": "^2.3.0",
- "@dfinity/principal": "^2.3.0",
- "@dfinity/vetkeys": "^0.3.0",
+ "@icp-sdk/auth": "^7.1.0",
+ "@icp-sdk/core": "^5.4.0",
+ "@icp-sdk/vetkeys": "^0.5.0-beta.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"daisyui": "^4.12.23",
"save": "^2.9.0",
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/scripts/gen_bindings.sh b/rust/vetkeys/password_manager_with_metadata/frontend/scripts/gen_bindings.sh
index e8f13011b..624b67835 100755
--- a/rust/vetkeys/password_manager_with_metadata/frontend/scripts/gen_bindings.sh
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/scripts/gen_bindings.sh
@@ -1,15 +1,17 @@
#!/bin/bash
+# Bindings are always generated from the Rust backend since both backends
+# expose the same Candid interface.
-cd ../../backend && make extract-candid
-
-cd .. && dfx generate password_manager_with_metadata || exit 1
-
-rm -r frontend/src/declarations/password_manager_with_metadata > /dev/null 2>&1 || true
+# Resolve the physical path of this script so that navigating up works
+# correctly even when frontend/ is reached via a symlink (e.g. motoko/frontend).
+SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd -P)
+if command -v candid-extractor >/dev/null 2>&1; then
+ cd "$SCRIPT_DIR/../../rust/backend" && make extract-candid
+fi
+cd "$SCRIPT_DIR/../.."
+rm -rf frontend/src/declarations/password_manager_with_metadata
mkdir -p frontend/src/declarations/password_manager_with_metadata
-mv src/declarations/password_manager_with_metadata frontend/src/declarations
-rmdir -p src/declarations > /dev/null 2>&1 || true
-
-# dfx 0.31+ generates @icp-sdk/core imports; rewrite to @dfinity/* to match deps
-find frontend/src/declarations -type f \( -name '*.ts' -o -name '*.js' \) -exec \
- perl -i -pe 's|\@icp-sdk/core/agent|\@dfinity/agent|g; s|\@icp-sdk/core/principal|\@dfinity/principal|g; s|\@icp-sdk/core/candid|\@dfinity/candid|g' {} +
\ No newline at end of file
+npx --yes @icp-sdk/bindgen --did-file rust/backend/backend.did \
+ --out-dir frontend/src/declarations/password_manager_with_metadata \
+ --declarations-flat --force
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/DisclaimerCopy.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/DisclaimerCopy.svelte
index 336ffde7d..7cf597760 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/DisclaimerCopy.svelte
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/DisclaimerCopy.svelte
@@ -1,5 +1,5 @@
-Disclaimer: This sample dapp is intended exclusively for experimental
-purpose. You are advised not to use this dapp for storing your critical data such
+Disclaimer: This sample app is intended exclusively for experimental
+purpose. You are advised not to use this app for storing your critical data such
as keys or passwords.
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditPassword.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditPassword.svelte
index b99631a3d..2f250d92e 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditPassword.svelte
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditPassword.svelte
@@ -11,8 +11,8 @@
import { auth } from "../store/auth";
import Spinner from "./Spinner.svelte";
import { onDestroy } from "svelte";
- import { Principal } from "@dfinity/principal";
- import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps";
+ import { Principal } from "@icp-sdk/core/principal";
+ import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps";
export let currentRoute = "";
const unsubscribe = location.subscribe((value) => {
@@ -71,7 +71,7 @@
v.owner.compareTo(parentVaultOwnerPrincipal) === "eq" &&
v.name === parentVaultName,
);
- const me = $auth.client.getIdentity().getPrincipal();
+ const me = $auth.principal;
if (
parentVaultOwnerPrincipal.compareTo(me) !== "eq" &&
(!vault ||
@@ -151,10 +151,9 @@
message: "Password saved successfully",
});
- await refreshVaults(
- $auth.client.getIdentity().getPrincipal(),
- $auth.passwordManager,
- ).catch((e) => showError(e as Error, "Could not refresh passwords."));
+ await refreshVaults($auth.principal, $auth.passwordManager).catch((e) =>
+ showError(e as Error, "Could not refresh passwords."),
+ );
if (move) {
void replace(
@@ -184,10 +183,7 @@
showError(e as Error, "Could not delete password.");
});
- await refreshVaults(
- $auth.client.getIdentity().getPrincipal(),
- $auth.passwordManager,
- )
+ await refreshVaults($auth.principal, $auth.passwordManager)
.catch((e) => showError(e as Error, "Could not refresh passwords."))
.finally(() => {
addNotification({
@@ -228,12 +224,11 @@
tagsInput = tags.join(", ");
}
- const myPrincipal = $auth.client.getIdentity().getPrincipal();
-
+ const myPrincipal = $auth.principal;
if (parentVaultOwnerPrincipal.compareTo(myPrincipal) === "eq") {
accessRights = { ReadWriteManage: null };
} else {
- let foundAccessRights = targetVault.users.find(
+ const foundAccessRights = targetVault.users.find(
(u) => u[0].compareTo(myPrincipal) === "eq",
);
if (foundAccessRights) {
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditVault.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditVault.svelte
index 7a3695666..c671bb0fe 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditVault.svelte
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditVault.svelte
@@ -6,8 +6,15 @@
import Trash from "svelte-icons/fa/FaTrash.svelte";
import { auth } from "../store/auth";
import Spinner from "./Spinner.svelte";
+ import { location } from "svelte-spa-router";
+ import { Principal } from "@icp-sdk/core/principal";
+ import { onDestroy } from "svelte";
- export let currentRoute = "";
+ let currentRoute = "";
+ const unsubscribeCurrentRoute = location.subscribe((value) => {
+ currentRoute = decodeURI(value);
+ });
+ onDestroy(unsubscribeCurrentRoute);
let editedVault: VaultModel;
let updating = false;
@@ -20,15 +27,21 @@
if (
$auth.state === "initialized" &&
$vaultsStore.state === "loaded" &&
- !editedVault
+ !editedVault &&
+ currentRoute.split("/").length > 2
) {
+ const split = currentRoute.split("/");
+ const vaultOwner = Principal.fromText(split[split.length - 2]);
+ const vaultName = split[split.length - 1];
const vault = $vaultsStore.list.find(
- (vault) => vault.name === currentRoute,
+ (v) =>
+ v.owner.compareTo(vaultOwner) === "eq" &&
+ v.name === vaultName,
);
if (vault) {
editedVault = { ...vault };
- const me = $auth.client.getIdentity().getPrincipal();
+ const me = $auth.principal;
canManage =
vault.owner.compareTo(me) === "eq" ||
"ReadWriteManage" in
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/NewPassword.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/NewPassword.svelte
index 998df9cf7..4dc1a099e 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/NewPassword.svelte
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/NewPassword.svelte
@@ -7,12 +7,12 @@
import { addNotification, showError } from "../store/notifications";
import Header from "./Header.svelte";
import PasswordEditor from "./PasswordEditor.svelte";
- import { Principal } from "@dfinity/principal";
+ import { Principal } from "@icp-sdk/core/principal";
let creating = false;
let vaultOwner =
$auth.state === "initialized"
- ? $auth.client.getIdentity().getPrincipal().toText()
+ ? $auth.principal.toText()
: Principal.anonymous().toText();
let vaultName = "";
let passwordName = "";
@@ -45,6 +45,14 @@
return;
}
+ if (vaultName.trim() === "" || passwordName.trim() === "") {
+ addNotification({
+ type: "error",
+ message: "Vault name and password name must not be empty.",
+ });
+ return;
+ }
+
creating = true;
await setPassword(
@@ -72,10 +80,9 @@
});
// refresh passwords in the background
- refreshVaults(
- $auth.client.getIdentity().getPrincipal(),
- $auth.passwordManager,
- ).catch((e: Error) => showError(e, "Could not refresh passwords."));
+ refreshVaults($auth.principal, $auth.passwordManager).catch(
+ (e: Error) => showError(e, "Could not refresh passwords."),
+ );
}
function saveDraft() {
@@ -131,7 +138,9 @@
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Password.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Password.svelte
index 1631811d5..0bfe57331 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Password.svelte
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Password.svelte
@@ -2,7 +2,7 @@
import { type PasswordModel, summarize } from "../lib/password";
import { link, location } from "svelte-spa-router";
import { vaultsStore } from "../store/vaults";
- import { Principal } from "@dfinity/principal";
+ import { Principal } from "@icp-sdk/core/principal";
import { onDestroy } from "svelte";
import Spinner from "./Spinner.svelte";
import Header from "./Header.svelte";
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SharingEditor.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SharingEditor.svelte
index 6e7f0b2a8..f3fe7b565 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SharingEditor.svelte
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SharingEditor.svelte
@@ -8,8 +8,8 @@
vaultsStore,
} from "../store/vaults";
import { addNotification, showError } from "../store/notifications";
- import { Principal } from "@dfinity/principal";
- import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps";
+ import { Principal } from "@icp-sdk/core/principal";
+ import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps";
export let editedVault: VaultModel;
export let canManage = false;
@@ -62,10 +62,9 @@
} finally {
adding = false;
}
- await refreshVaults(
- $auth.client.getIdentity().getPrincipal(),
- $auth.passwordManager,
- ).catch((e: Error) => showError(e, "Could not refresh vaults."));
+ await refreshVaults($auth.principal, $auth.passwordManager).catch(
+ (e: Error) => showError(e, "Could not refresh vaults."),
+ );
}
async function remove(sharing: Principal) {
@@ -92,10 +91,9 @@
} finally {
removing = false;
}
- await refreshVaults(
- $auth.client.getIdentity().getPrincipal(),
- $auth.passwordManager,
- ).catch((e: Error) => showError(e, "Could not refresh vaults."));
+ await refreshVaults($auth.principal, $auth.passwordManager).catch(
+ (e: Error) => showError(e, "Could not refresh vaults."),
+ );
}
function onKeyPress(e: KeyboardEvent) {
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SidebarLayout.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SidebarLayout.svelte
index 439db77a7..94a1e1fe5 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SidebarLayout.svelte
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SidebarLayout.svelte
@@ -7,7 +7,7 @@
// @ts-expect-error: svelte-icons have some problems with ts declarations
import FaDoorOpen from "svelte-icons/fa/FaDoorOpen.svelte";
import Disclaimer from "./Disclaimer.svelte";
- import { Principal } from "@dfinity/principal";
+ import { Principal } from "@icp-sdk/core/principal";
import { link } from "svelte-spa-router";
@@ -34,7 +34,7 @@
My Principal:
{$auth.state === "initialized"
- ? $auth.client.getIdentity().getPrincipal().toText()
+ ? $auth.principal.toText()
: Principal.anonymous().toText()}
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Vault.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Vault.svelte
index 4ce52a0d4..378351761 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Vault.svelte
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Vault.svelte
@@ -3,14 +3,14 @@
import { link, location } from "svelte-spa-router";
import { onDestroy } from "svelte";
import { vaultsStore } from "../store/vaults";
- import { Principal } from "@dfinity/principal";
+ import { Principal } from "@icp-sdk/core/principal";
import Header from "./Header.svelte";
import Spinner from "./Spinner.svelte";
// @ts-expect-error: svelte-icons have some problems with ts declarations
import GiOpenTreasureChest from "svelte-icons/gi/GiOpenTreasureChest.svelte";
import { auth } from "../store/auth";
import SharingEditor from "./SharingEditor.svelte";
- import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps";
+ import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps";
export let vault: VaultModel = {
name: "",
@@ -27,42 +27,46 @@
});
onDestroy(unsubscribeCurrentRoute);
- $: {
- if (
- $vaultsStore.state === "loaded" &&
- $auth.state === "initialized" &&
- vault.name.length === 0 &&
- currentRoute.split("/").length > 2
- ) {
- const split = currentRoute.split("/");
- const vaultName = split[split.length - 1];
- const vaultOwner = Principal.fromText(split[split.length - 2]);
- const searchedForVault = $vaultsStore.list.find(
- (v) =>
- v.owner.compareTo(vaultOwner) === "eq" &&
- v.name === vaultName,
- );
- if (!searchedForVault) {
- vaultSummary =
- "could not find vault " +
- vaultName +
- " owned by " +
- vaultOwner.toText();
- } else {
- vault = searchedForVault;
- vaultSummary += summarize(vault);
- const me = $auth.client.getIdentity().getPrincipal();
+ // Parse owner and vault name from the URL once; stored separately so the
+ // vault lookup below stays reactive to store updates (e.g. from the poller).
+ let parsedRoute: { owner: Principal; vaultName: string } | null = null;
+ $: if (currentRoute.split("/").length > 2 && parsedRoute === null) {
+ const split = currentRoute.split("/");
+ parsedRoute = {
+ owner: Principal.fromText(split[split.length - 2]),
+ vaultName: split[split.length - 1], // already decoded via decodeURI on subscribe
+ };
+ }
- if (vault.owner.compareTo(me) === "eq") {
- accessRights = { ReadWriteManage: null };
- } else {
- const foundRights = vault.users.find(
- (user) => user[0].compareTo(me) === "eq",
- );
- accessRights = foundRights
- ? foundRights[1]
- : { Read: null };
- }
+ // Re-runs whenever the store updates so new passwords from the poller appear.
+ $: if (
+ $vaultsStore.state === "loaded" &&
+ $auth.state === "initialized" &&
+ parsedRoute !== null
+ ) {
+ const { owner: targetOwner, vaultName: targetVaultName } = parsedRoute;
+ const searchedForVault = $vaultsStore.list.find(
+ (v) =>
+ v.owner.compareTo(targetOwner) === "eq" &&
+ v.name === targetVaultName,
+ );
+ if (!searchedForVault) {
+ vaultSummary =
+ "could not find vault " +
+ targetVaultName +
+ " owned by " +
+ targetOwner.toText();
+ } else {
+ vault = searchedForVault;
+ vaultSummary = summarize(vault);
+ const me = $auth.principal;
+ if (vault.owner.compareTo(me) === "eq") {
+ accessRights = { ReadWriteManage: null };
+ } else {
+ const foundRights = vault.users.find(
+ (user) => user[0].compareTo(me) === "eq",
+ );
+ accessRights = foundRights ? foundRights[1] : { Read: null };
}
}
}
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts
index 543f05f10..e464c683c 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts
@@ -1,31 +1,22 @@
-import "./init.ts";
-import { HttpAgent, type HttpAgentOptions } from "@dfinity/agent";
+import { HttpAgent } from "@icp-sdk/core/agent";
import {
DefaultEncryptedMapsClient,
EncryptedMaps,
-} from "@dfinity/vetkeys/encrypted_maps";
+} from "@icp-sdk/vetkeys/encrypted_maps";
+import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
-export async function createEncryptedMaps(
- agentOptions: HttpAgentOptions,
-): Promise {
- const agent = await HttpAgent.create({ ...agentOptions });
- // Fetch root key for certificate validation during development
- if (process.env.NODE_ENV !== "production") {
- console.log(`Dev environment - fetching root key...`);
+const canisterEnv = safeGetCanisterEnv<{
+ "PUBLIC_CANISTER_ID:password_manager_with_metadata": string;
+}>();
- agent.fetchRootKey().catch((err) => {
- console.warn(
- "Unable to fetch root key. Check to ensure that your local replica is running",
- );
- console.error(err);
- });
+export function createEncryptedMaps(agent: HttpAgent): EncryptedMaps {
+ const canisterId =
+ canisterEnv?.["PUBLIC_CANISTER_ID:password_manager_with_metadata"];
+ if (!canisterId) {
+ throw new Error(
+ "Canister ID for password_manager_with_metadata is not set",
+ );
}
- // Creates an actor with using the candid interface and the HttpAgent
- return new EncryptedMaps(
- new DefaultEncryptedMapsClient(
- agent,
- process.env.CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA as string,
- ),
- );
+ return new EncryptedMaps(new DefaultEncryptedMapsClient(agent, canisterId));
}
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/init.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/init.ts
index 062c8af94..e69de29bb 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/init.ts
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/init.ts
@@ -1 +0,0 @@
-window.global ||= window;
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password.ts
index 88620bd13..ff7d78287 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password.ts
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password.ts
@@ -1,5 +1,5 @@
-import type { Principal } from "@dfinity/principal";
-import type { PasswordMetadata } from "../declarations/password_manager_with_metadata/password_manager_with_metadata.did";
+import type { Principal } from "@icp-sdk/core/principal";
+import type { PasswordMetadata } from "../declarations/password_manager_with_metadata/backend.did";
export interface PasswordModel {
owner: Principal;
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password_manager.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password_manager.ts
index 232f1873a..fe216a638 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password_manager.ts
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password_manager.ts
@@ -1,13 +1,19 @@
-import "./init.ts";
-import { type ActorSubclass, type HttpAgentOptions } from "@dfinity/agent";
-import { EncryptedMaps } from "@dfinity/vetkeys/encrypted_maps";
+import { Actor, HttpAgent, type ActorSubclass } from "@icp-sdk/core/agent";
+import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
+import type { Principal } from "@icp-sdk/core/principal";
+import { EncryptedMaps } from "@icp-sdk/vetkeys/encrypted_maps";
+import {
+ idlFactory,
+ type _SERVICE,
+} from "../declarations/password_manager_with_metadata/backend.did";
import { createEncryptedMaps } from "./encrypted_maps";
-import type { Principal } from "@dfinity/principal";
-import { createActor } from "../declarations/password_manager_with_metadata";
-import type { _SERVICE } from "../declarations/password_manager_with_metadata/password_manager_with_metadata.did";
import { passwordFromContent, type PasswordModel } from "../lib/password";
import { vaultFromContent, type VaultModel } from "../lib/vault";
+const canisterEnv = safeGetCanisterEnv<{
+ "PUBLIC_CANISTER_ID:password_manager_with_metadata": string;
+}>();
+
export class PasswordManager {
/// The actor class representing the full interface of the canister.
private readonly canisterClient: ActorSubclass<_SERVICE>;
@@ -145,35 +151,28 @@ export class PasswordManager {
}
}
-export async function createPasswordManager(
- agentOptions?: HttpAgentOptions,
-): Promise {
- if (!process.env.CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA) {
- console.error(
- "CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA is not defined",
- );
+export async function createPasswordManager(agentOptions?: {
+ identity?: HttpAgent["config"]["identity"];
+}): Promise {
+ const canisterId =
+ canisterEnv?.["PUBLIC_CANISTER_ID:password_manager_with_metadata"];
+ if (!canisterId) {
throw new Error(
- "CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA is not defined",
+ "Canister ID for password_manager_with_metadata is not defined",
);
}
- const host =
- process.env.DFX_NETWORK === "ic"
- ? `https://${process.env.CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA}.ic0.app`
- : "http://localhost:8000";
- const hostOptions = { host };
-
- if (!agentOptions) {
- agentOptions = hostOptions;
- } else {
- agentOptions.host = hostOptions.host;
- }
-
- const encryptedMaps = await createEncryptedMaps({ ...agentOptions });
- const canisterClient = createActor(
- process.env.CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA,
- { agentOptions },
- );
+ const agent = await HttpAgent.create({
+ ...agentOptions,
+ host: window.location.origin,
+ rootKey: canisterEnv?.IC_ROOT_KEY,
+ });
+
+ const encryptedMaps = createEncryptedMaps(agent);
+ const canisterClient = Actor.createActor<_SERVICE>(idlFactory, {
+ agent,
+ canisterId,
+ });
return new PasswordManager(canisterClient, encryptedMaps);
}
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/vault.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/vault.ts
index 717fc4767..49d302a8d 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/vault.ts
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/vault.ts
@@ -1,6 +1,6 @@
-import type { Principal } from "@dfinity/principal";
+import type { Principal } from "@icp-sdk/core/principal";
import type { PasswordModel } from "./password";
-import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps";
+import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps";
export interface VaultModel {
owner: Principal;
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts
index aa05921c4..1f54fb574 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts
@@ -1,7 +1,7 @@
-import "../lib/init.ts";
import { get, writable } from "svelte/store";
-import { AuthClient } from "@dfinity/auth-client";
-import type { JsonnableDelegationChain } from "@dfinity/identity/lib/cjs/identity/delegation";
+import { AuthClient, LocalStorage } from "@icp-sdk/auth/client";
+import { DelegationIdentity } from "@icp-sdk/core/identity";
+import type { Principal } from "@icp-sdk/core/principal";
import { replace } from "svelte-spa-router";
import {
PasswordManager,
@@ -20,6 +20,7 @@ export type AuthState =
state: "initialized";
passwordManager: PasswordManager;
client: AuthClient;
+ principal: Principal;
}
| {
state: "error";
@@ -31,8 +32,22 @@ export const auth = writable({
});
async function initAuth() {
- const client = await AuthClient.create();
- if (await client.isAuthenticated()) {
+ const isLocalEnv =
+ window.location.hostname === "localhost" ||
+ window.location.hostname.endsWith(".localhost");
+ // Workaround for https://github.com/dfinity/icp-js-auth/issues/120
+ // IdbStorage has a race condition on localhost dev servers. LocalStorage
+ // avoids IDB on local but uses plain string storage (less secure), so
+ // production deployments keep the default secure IdbStorage + ECDSA key.
+ const client = new AuthClient({
+ identityProvider: isLocalEnv
+ ? "http://id.ai.localhost:8000/authorize"
+ : "https://id.ai/authorize",
+ ...(isLocalEnv
+ ? { storage: new LocalStorage(), keyType: "Ed25519" as const }
+ : {}),
+ });
+ if (client.isAuthenticated()) {
await authenticate(client);
} else {
auth.update(() => ({
@@ -44,24 +59,20 @@ async function initAuth() {
void initAuth();
-export async function login() {
+export function login() {
const currentAuth = get(auth);
if (currentAuth.state === "anonymous") {
- await currentAuth.client.login({
- maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000),
- identityProvider:
- process.env.DFX_NETWORK === "ic"
- ? "https://identity.ic0.app/#authorize"
- : `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:8000/#authorize`,
- onSuccess: async () => {
- await authenticate(currentAuth.client);
- },
- onError: (e) =>
- console.error(
- "Failed to authenticate with internet identity: " + e,
- ),
- });
+ void (async () => {
+ try {
+ await currentAuth.client.signIn({
+ maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000),
+ });
+ void authenticate(currentAuth.client);
+ } catch (error: unknown) {
+ console.error("Login failed:", error);
+ }
+ })();
}
}
@@ -69,7 +80,7 @@ export async function logout() {
const currentAuth = get(auth);
if (currentAuth.state === "initialized") {
- await currentAuth.client.logout();
+ await currentAuth.client.signOut();
auth.update(() => ({
state: "anonymous",
client: currentAuth.client,
@@ -79,17 +90,17 @@ export async function logout() {
}
export async function authenticate(client: AuthClient) {
- handleSessionTimeout();
+ void handleSessionTimeout(client);
try {
- const passwordManager = await createPasswordManager({
- identity: client.getIdentity(),
- });
+ const identity = await client.getIdentity();
+ const passwordManager = await createPasswordManager({ identity });
auth.update(() => ({
state: "initialized",
passwordManager,
client,
+ principal: identity.getPrincipal(),
}));
} catch (e) {
auth.update(() => ({
@@ -100,31 +111,20 @@ export async function authenticate(client: AuthClient) {
}
// set a timer when the II session will expire and log the user out
-function handleSessionTimeout() {
- // upon login the localstorage items may not be set, wait for next tick
- setTimeout(() => {
- try {
- const rawDelegation = window.localStorage.getItem("ic-delegation");
- if (!rawDelegation) {
- throw new Error("No delegation found");
- }
- const delegation = JSON.parse(
- rawDelegation,
- ) as JsonnableDelegationChain;
+async function handleSessionTimeout(client: AuthClient) {
+ try {
+ const identity = await client.getIdentity();
+ if (!(identity instanceof DelegationIdentity)) return;
- const expirationTimeMs =
- Number.parseInt(
- delegation.delegations[0].delegation.expiration,
- 16,
- ) / 1000000;
+ const chain = identity.getDelegation();
+ // expiration is a BigInt of nanoseconds since epoch
+ const expirationMs =
+ Number(chain.delegations[0].delegation.expiration) / 1_000_000;
- setTimeout(() => {
- void logout();
- }, expirationTimeMs - Date.now());
- } catch (e) {
- console.error(
- "Could not handle delegation expiry: " + (e as Error).message,
- );
- }
- });
+ setTimeout(() => {
+ void logout();
+ }, expirationMs - Date.now());
+ } catch {
+ console.error("Could not handle delegation expiry.");
+ }
}
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/vaults.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/vaults.ts
index a236a9559..f56f5dcb8 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/vaults.ts
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/vaults.ts
@@ -3,8 +3,8 @@ import { type PasswordModel } from "../lib/password";
import { type VaultModel } from "../lib/vault";
import { auth } from "./auth";
import { showError } from "./notifications";
-import { type AccessRights } from "@dfinity/vetkeys/encrypted_maps";
-import type { Principal } from "@dfinity/principal";
+import { type AccessRights } from "@icp-sdk/vetkeys/encrypted_maps";
+import type { Principal } from "@icp-sdk/core/principal";
import type { PasswordManager } from "../lib/password_manager";
export const vaultsStore = writable<
@@ -119,14 +119,13 @@ auth.subscribe((auth) => {
void (async () => {
try {
- await refreshVaults(
- auth.client.getIdentity().getPrincipal(),
- auth.passwordManager,
- ).catch((e) => showError(e as Error, "Could not poll vaults."));
+ await refreshVaults(auth.principal, auth.passwordManager).catch(
+ (e) => showError(e as Error, "Could not poll vaults."),
+ );
vaultPollerHandle = setInterval(() => {
void refreshVaults(
- auth.client.getIdentity().getPrincipal(),
+ auth.principal,
auth.passwordManager,
).catch((e) =>
showError(e as Error, "Could not poll vaults."),
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/tailwind.config.cjs b/rust/vetkeys/password_manager_with_metadata/frontend/tailwind.config.mjs
similarity index 100%
rename from rust/vetkeys/password_manager_with_metadata/frontend/tailwind.config.cjs
rename to rust/vetkeys/password_manager_with_metadata/frontend/tailwind.config.mjs
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/vite.config.js b/rust/vetkeys/password_manager_with_metadata/frontend/vite.config.js
index d8ee7a41a..ca53dc054 100644
--- a/rust/vetkeys/password_manager_with_metadata/frontend/vite.config.js
+++ b/rust/vetkeys/password_manager_with_metadata/frontend/vite.config.js
@@ -4,23 +4,53 @@ import eslint from "vite-plugin-eslint";
import tailwindcss from "tailwindcss";
import autoprefixer from "autoprefixer";
import css from "rollup-plugin-css-only";
-import typescript from "@rollup/plugin-typescript";
import viteCompression from "vite-plugin-compression";
-import environment from "vite-plugin-environment";
-import path from "path";
+import { execSync } from "child_process";
+
+const environment = process.env.ICP_ENVIRONMENT || "local";
+const CANISTER_NAMES = ["password_manager_with_metadata"];
+
+function getDevServerConfig() {
+ const backend = process.env.BACKEND;
+ if (!backend) {
+ throw new Error(
+ "BACKEND env var is required. Use `npm run dev:motoko` or `npm run dev:rust`.",
+ );
+ }
+
+ const networkStatus = JSON.parse(
+ execSync(
+ `icp network status -e ${environment} --json --project-root-override ../${backend}`,
+ { encoding: "utf-8" },
+ ),
+ );
+ const canisterParams = CANISTER_NAMES.map((name) => {
+ const id = execSync(
+ `icp canister status ${name} -e ${environment} --id-only --project-root-override ../${backend}`,
+ { encoding: "utf-8", stdio: "pipe" },
+ ).trim();
+ return `PUBLIC_CANISTER_ID:${name}=${id}`;
+ }).join("&");
+ return {
+ headers: {
+ "Set-Cookie": `ic_env=${encodeURIComponent(
+ `${canisterParams}&ic_root_key=${networkStatus.root_key}`,
+ )}; SameSite=Lax;`,
+ },
+ proxy: {
+ "/api": { target: networkStatus.api_url, changeOrigin: true },
+ },
+ hmr: false,
+ };
+}
// https://vite.dev/config/
-export default defineConfig({
+export default defineConfig(({ command }) => ({
plugins: [
svelte(),
css({ output: "bundle.css" }),
eslint(),
- typescript({
- inlineSources: true,
- }),
viteCompression(),
- environment("all", { prefix: "CANISTER_" }),
- environment("all", { prefix: "DFX_" }),
],
css: {
postcss: {
@@ -32,31 +62,14 @@ export default defineConfig({
output: {
inlineDynamicImports: true,
},
- sourcemap: true,
- },
- build: {
- rollupOptions: {
- output: {
- inlineDynamicImports: true,
- },
- sourcemap: true,
- },
- },
- root: "./",
- server: {
- hmr: false,
},
+ sourcemap: true,
},
+ root: "./",
resolve: {
alias: {
- ic_vetkeys: path.resolve(
- __dirname,
- "../../../frontend/ic_vetkeys/src",
- ),
- "ic_vetkeys/encrypted_maps": path.resolve(
- __dirname,
- "../../../frontend/ic_vetkeys/src/encrypted_maps",
- ),
+ "@dfinity/vetkeys": "@icp-sdk/vetkeys",
},
},
-});
+ ...(command === "serve" ? { server: getDevServerConfig() } : {}),
+}));
diff --git a/rust/vetkeys/password_manager_with_metadata/motoko/backend/src/Main.mo b/rust/vetkeys/password_manager_with_metadata/motoko/backend/src/Main.mo
index 6cfa11b37..b13b3f6a0 100644
--- a/rust/vetkeys/password_manager_with_metadata/motoko/backend/src/Main.mo
+++ b/rust/vetkeys/password_manager_with_metadata/motoko/backend/src/Main.mo
@@ -1,14 +1,15 @@
-import Principal "mo:base/Principal";
-import Blob "mo:base/Blob";
-import Buffer "mo:base/Buffer";
-import Array "mo:base/Array";
-import OrderedMap "mo:base/OrderedMap";
-import MotokoResult "mo:base/Result";
-import Text "mo:base/Text";
-import Time "mo:base/Time";
-import Nat64 "mo:base/Nat64";
-import Int "mo:base/Int";
-import Debug "mo:base/Debug";
+import Principal "mo:core/Principal";
+import Blob "mo:core/Blob";
+import List "mo:core/List";
+import Array "mo:core/Array";
+import OrderedMap "mo:core/pure/Map";
+import MotokoResult "mo:core/Result";
+import Text "mo:core/Text";
+import Time "mo:core/Time";
+import Nat64 "mo:core/Nat64";
+import Int "mo:core/Int";
+import Debug "mo:core/Debug";
+import Runtime "mo:core/Runtime";
import VetKeys "mo:ic-vetkeys";
persistent actor class (keyName : Text) {
@@ -34,8 +35,7 @@ persistent actor class (keyName : Text) {
ownerCompare;
};
};
- transient let metadataMapOps = OrderedMap.Make(compareMetadataKeys);
- var metadata : OrderedMap.Map = metadataMapOps.empty();
+ var metadata : OrderedMap.Map = OrderedMap.empty();
// Types
public type PasswordMetadata = {
@@ -116,21 +116,21 @@ persistent actor class (keyName : Text) {
switch (encryptedMaps.getEncryptedValuesForMap(caller, mapId)) {
case (#err(msg)) { #Err(msg) };
case (#ok(mapValues)) {
- let results = Buffer.Buffer<(ByteBuf, ByteBuf, PasswordMetadata)>(0);
+ let results = List.empty<(ByteBuf, ByteBuf, PasswordMetadata)>();
- for ((key, encryptedValue) in mapValues.vals()) {
+ for ((key, encryptedValue) in mapValues.values()) {
let metadataKey = (map_owner, map_name.inner, key);
- switch (metadataMapOps.get(metadata, metadataKey)) {
+ switch (OrderedMap.get(metadata, compareMetadataKeys,metadataKey)) {
case (null) {
- Debug.trap("bug: inconsistent state: no metadata for key");
+ Runtime.trap("bug: inconsistent state: no metadata for key");
};
case (?metadataValue) {
- results.add(({ inner = key }, { inner = encryptedValue }, metadataValue));
+ List.add(results, ({ inner = key }, { inner = encryptedValue }, metadataValue));
};
};
};
- #Ok(Buffer.toArray(results));
+ #Ok(List.toArray(results));
};
};
};
@@ -156,7 +156,7 @@ persistent actor class (keyName : Text) {
case (#err(msg)) { #Err(msg) };
case (#ok(optPrevValue)) {
let metadataKey = (map_owner, map_name.inner, map_key.inner);
- let prevMetadata = metadataMapOps.get(metadata, metadataKey);
+ let prevMetadata = OrderedMap.get(metadata, compareMetadataKeys,metadataKey);
let metadataValue = switch (prevMetadata) {
case (null) {
@@ -167,15 +167,15 @@ persistent actor class (keyName : Text) {
};
};
- metadata := metadataMapOps.put(metadata, metadataKey, metadataValue);
+ metadata := OrderedMap.add(metadata, compareMetadataKeys,metadataKey, metadataValue);
switch (optPrevValue, prevMetadata) {
case (null, null) { #Ok(null) };
case (null, ?_) {
- Debug.trap("bug: inconsistent state: no previous value but some metadata");
+ Runtime.trap("bug: inconsistent state: no previous value but some metadata");
};
case (?_, null) {
- Debug.trap("bug: inconsistent state: some previous value but no metadata");
+ Runtime.trap("bug: inconsistent state: some previous value but no metadata");
};
case (?prevValue, ?m) { #Ok(?({ inner = prevValue }, m)) };
};
@@ -194,17 +194,17 @@ persistent actor class (keyName : Text) {
case (#err(msg)) { #Err(msg) };
case (#ok(optPrevValue)) {
let metadataKey = (map_owner, map_name.inner, map_key.inner);
- let prevMetadata = metadataMapOps.get(metadata, metadataKey);
+ let prevMetadata = OrderedMap.get(metadata, compareMetadataKeys,metadataKey);
- metadata := metadataMapOps.delete(metadata, metadataKey);
+ metadata := OrderedMap.remove(metadata, compareMetadataKeys,metadataKey);
switch (optPrevValue, prevMetadata) {
case (null, null) { #Ok(null) };
case (null, ?_) {
- Debug.trap("bug: inconsistent state: no previous value but some metadata");
+ Runtime.trap("bug: inconsistent state: no previous value but some metadata");
};
case (?_, null) {
- Debug.trap("bug: inconsistent state: some previous value but no metadata");
+ Runtime.trap("bug: inconsistent state: some previous value but no metadata");
};
case (?prevValue, ?m) { #Ok(?({ inner = prevValue }, m)) };
};
diff --git a/rust/vetkeys/password_manager_with_metadata/motoko/dfx.json b/rust/vetkeys/password_manager_with_metadata/motoko/dfx.json
deleted file mode 100644
index 1cf398f72..000000000
--- a/rust/vetkeys/password_manager_with_metadata/motoko/dfx.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "canisters": {
- "password_manager_with_metadata": {
- "main": "backend/src/Main.mo",
- "args": "--enhanced-orthogonal-persistence",
- "type": "motoko",
- "init_arg": "(\"test_key_1\")",
- "metadata": [
- {
- "name": "candid:service",
- "visibility": "public"
- }
- ]
- },
- "internet-identity": {
- "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity.did",
- "type": "custom",
- "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai",
- "remote": {
- "id": {
- "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai"
- }
- },
- "wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity_dev.wasm.gz"
- },
- "www": {
- "dependencies": ["password_manager_with_metadata", "internet-identity"],
- "build": ["cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./"],
- "frontend": {
- "entrypoint": "dist/index.html"
- },
- "source": ["dist/"],
- "type": "assets",
- "output_env_file": "frontend/.env"
- }
- },
- "defaults": {
- "build": {
- "packtool": "npx ic-mops sources",
- "args": ""
- }
- },
- "networks": {
- "local": {
- "bind": "127.0.0.1:8000",
- "type": "ephemeral"
- }
- }
- }
-
\ No newline at end of file
diff --git a/rust/vetkeys/password_manager_with_metadata/motoko/icp.yaml b/rust/vetkeys/password_manager_with_metadata/motoko/icp.yaml
new file mode 100644
index 000000000..c19cfbed6
--- /dev/null
+++ b/rust/vetkeys/password_manager_with_metadata/motoko/icp.yaml
@@ -0,0 +1,22 @@
+canisters:
+ - name: password_manager_with_metadata
+ recipe:
+ type: "@dfinity/motoko@v4.1.0"
+ configuration:
+ main: backend/src/Main.mo
+ init_args:
+ type: text
+ value: "(\"test_key_1\")"
+
+ - name: www
+ recipe:
+ type: "@dfinity/asset-canister@v2.1.0"
+ configuration:
+ dir: dist
+ build:
+ - cd frontend && npm i --include=dev && npm run build && cd - && rm -rf dist; mv frontend/dist ./
+
+networks:
+ - name: local
+ mode: managed
+ ii: true
diff --git a/rust/vetkeys/password_manager_with_metadata/motoko/mops.toml b/rust/vetkeys/password_manager_with_metadata/motoko/mops.toml
index f8b191454..670c65a28 100644
--- a/rust/vetkeys/password_manager_with_metadata/motoko/mops.toml
+++ b/rust/vetkeys/password_manager_with_metadata/motoko/mops.toml
@@ -1,3 +1,6 @@
+[toolchain]
+moc = "1.9.0"
+
[dependencies]
-base = "0.14.9"
-ic-vetkeys = "0.4.0"
\ No newline at end of file
+core = "2.5.0"
+ic-vetkeys = "0.5.0"
\ No newline at end of file
diff --git a/rust/vetkeys/password_manager_with_metadata/rust/backend/Cargo.toml b/rust/vetkeys/password_manager_with_metadata/rust/backend/Cargo.toml
index 097e3b72d..964961938 100644
--- a/rust/vetkeys/password_manager_with_metadata/rust/backend/Cargo.toml
+++ b/rust/vetkeys/password_manager_with_metadata/rust/backend/Cargo.toml
@@ -14,9 +14,10 @@ crate-type = ["cdylib"]
[dependencies]
candid = "0.10.2"
-ic-cdk = "0.18.3"
+ic-cdk = "0.20.1"
+ic-cdk-management-canister = "0.1.1"
ic-dummy-getrandom-for-wasm = "0.1.0"
-ic-stable-structures = "0.6.8"
-ic-vetkeys = "0.2.0"
+ic-stable-structures = "0.7.2"
+ic-vetkeys = "0.7.0"
serde = "1.0.217"
serde_cbor = "0.11.2"
diff --git a/rust/vetkeys/password_manager_with_metadata/rust/backend/Makefile b/rust/vetkeys/password_manager_with_metadata/rust/backend/Makefile
index c6860e235..fadd94355 100644
--- a/rust/vetkeys/password_manager_with_metadata/rust/backend/Makefile
+++ b/rust/vetkeys/password_manager_with_metadata/rust/backend/Makefile
@@ -12,4 +12,4 @@ extract-candid: compile-wasm
.SILENT: clean
clean:
cargo clean
- rm -rf ../.dfx
\ No newline at end of file
+ rm -rf ../.icp
\ No newline at end of file
diff --git a/rust/vetkeys/password_manager_with_metadata/rust/backend/backend.did b/rust/vetkeys/password_manager_with_metadata/rust/backend/backend.did
index 459b6c585..1b4e5f994 100644
--- a/rust/vetkeys/password_manager_with_metadata/rust/backend/backend.did
+++ b/rust/vetkeys/password_manager_with_metadata/rust/backend/backend.did
@@ -1,4 +1,15 @@
-type AccessRights = variant { Read; ReadWrite; ReadWriteManage };
+// Access rights of a user to a vetKey in [`crate::key_manager::KeyManager`] and/or an encrypted map in [`crate::encrypted_maps::EncryptedMaps`].
+type AccessRights = variant {
+ // User can retrieve the vetKey or encrypted map.
+ Read;
+ // User can update values in the encrypted map.
+ ReadWrite;
+ // User can view/share/revoke access to the vetKey or encrypted map.
+ ReadWriteManage;
+};
+// Efficiently serializable and deserializable byte vector that is `Storable` with `ic_stable_structures`.
+// See, e.g., [https://mmapped.blog/posts/01-effective-rust-canisters#serde-bytes](https://mmapped.blog/posts/01-effective-rust-canisters#serde-bytes) for more details regarding why `Vec` does not work out of the box.
+// Also, we cannot use `serde_bytes::ByteBuf` directly because it is not `Storable`.
type ByteBuf = record { inner : blob };
type PasswordMetadata = record {
url : text;
diff --git a/rust/vetkeys/password_manager_with_metadata/rust/backend/src/lib.rs b/rust/vetkeys/password_manager_with_metadata/rust/backend/src/lib.rs
index eec198ea0..334d0509b 100644
--- a/rust/vetkeys/password_manager_with_metadata/rust/backend/src/lib.rs
+++ b/rust/vetkeys/password_manager_with_metadata/rust/backend/src/lib.rs
@@ -1,10 +1,10 @@
use candid::{CandidType, Principal};
-use ic_cdk::management_canister::{VetKDCurve, VetKDKeyId};
-use ic_cdk::{init, query, update};
+use ic_cdk_management_canister::{VetKDCurve, VetKDKeyId};
+use ic_cdk::{init, post_upgrade, query, update};
use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory};
use ic_stable_structures::storable::Blob;
use ic_stable_structures::{storable::Bound, Storable};
-use ic_stable_structures::{BTreeMap as StableBTreeMap, DefaultMemoryImpl};
+use ic_stable_structures::{BTreeMap as StableBTreeMap, Cell as StableCell, DefaultMemoryImpl};
use ic_vetkeys::encrypted_maps::{EncryptedMaps, VetKey, VetKeyVerificationKey};
use ic_vetkeys::types::{AccessRights, ByteBuf, EncryptedMapValue, TransportKey};
use serde::{Deserialize, Serialize};
@@ -48,6 +48,10 @@ impl PasswordMetadata {
}
impl Storable for PasswordMetadata {
+ fn into_bytes(self) -> Vec {
+ self.to_bytes().into_owned()
+ }
+
fn to_bytes(&self) -> Cow<[u8]> {
Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize"))
}
@@ -75,17 +79,33 @@ thread_local! {
static METADATA: RefCell = RefCell::new(StableBTreeMap::new(
MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(4))),
));
+ static KEY_NAME: RefCell> =
+ RefCell::new(StableCell::init(
+ MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(5))),
+ String::new(),
+ ));
}
#[init]
fn init(key_name: String) {
+ KEY_NAME.with_borrow_mut(|k| { k.set(key_name.clone()); });
+ init_encrypted_maps(key_name);
+}
+
+#[post_upgrade]
+fn post_upgrade() {
+ let key_name = KEY_NAME.with_borrow(|k| k.get().clone());
+ init_encrypted_maps(key_name);
+}
+
+fn init_encrypted_maps(key_name: String) {
let key_id = VetKDKeyId {
curve: VetKDCurve::Bls12_381_G2,
name: key_name,
};
ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| {
encrypted_maps.replace(EncryptedMaps::init(
- "password_manager_dapp",
+ "password_manager_app",
key_id,
MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))),
MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))),
@@ -143,8 +163,11 @@ fn get_encrypted_values_for_map_with_metadata(
METADATA.with_borrow(|metadata| {
let iter_metadata = metadata
.range((map_owner, map_name, Blob::default())..)
- .take_while(|((owner, name, _), _)| owner == &map_owner && name == &map_name)
- .map(|((_, _, key), metadata)| (key, metadata));
+ .take_while(|entry| {
+ let (owner, name, _) = entry.key();
+ owner == &map_owner && name == &map_name
+ })
+ .map(|entry| (entry.key().2, entry.value()));
iter_metadata
.zip(map_values)
diff --git a/rust/vetkeys/password_manager_with_metadata/rust/dfx.json b/rust/vetkeys/password_manager_with_metadata/rust/dfx.json
deleted file mode 100644
index b361f8c27..000000000
--- a/rust/vetkeys/password_manager_with_metadata/rust/dfx.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "canisters": {
- "password_manager_with_metadata": {
- "candid": "backend/backend.did",
- "package": "ic-vetkd-example-password-manager-with-metadata-backend",
- "type": "rust",
- "init_arg": "(\"test_key_1\")",
- "metadata": [
- {
- "name": "candid:service",
- "visibility": "public"
- }
- ]
- },
- "internet-identity": {
- "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity.did",
- "type": "custom",
- "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai",
- "remote": {
- "id": {
- "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai"
- }
- },
- "wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity_dev.wasm.gz"
- },
- "www": {
- "dependencies": ["password_manager_with_metadata", "internet-identity"],
- "build": ["cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./"],
- "frontend": {
- "entrypoint": "dist/index.html"
- },
- "source": ["dist/"],
- "type": "assets",
- "output_env_file": "frontend/.env"
- }
- },
- "networks": {
- "local": {
- "bind": "127.0.0.1:8000",
- "type": "ephemeral"
- }
- }
-}
diff --git a/rust/vetkeys/password_manager_with_metadata/rust/icp.yaml b/rust/vetkeys/password_manager_with_metadata/rust/icp.yaml
new file mode 100644
index 000000000..9d753e1a4
--- /dev/null
+++ b/rust/vetkeys/password_manager_with_metadata/rust/icp.yaml
@@ -0,0 +1,23 @@
+canisters:
+ - name: password_manager_with_metadata
+ recipe:
+ type: "@dfinity/rust@v3.2.0"
+ configuration:
+ package: ic-vetkd-example-password-manager-with-metadata-backend
+ candid: backend/backend.did
+ init_args:
+ type: text
+ value: "(\"test_key_1\")"
+
+ - name: www
+ recipe:
+ type: "@dfinity/asset-canister@v2.1.0"
+ configuration:
+ dir: dist
+ build:
+ - cd frontend && npm i --include=dev && npm run build && cd - && rm -rf dist; mv frontend/dist ./
+
+networks:
+ - name: local
+ mode: managed
+ ii: true
diff --git a/rust/vetkeys/password_manager_with_metadata/rust/rust-toolchain.toml b/rust/vetkeys/password_manager_with_metadata/rust/rust-toolchain.toml
index 2a2058b04..6e5beca4f 100644
--- a/rust/vetkeys/password_manager_with_metadata/rust/rust-toolchain.toml
+++ b/rust/vetkeys/password_manager_with_metadata/rust/rust-toolchain.toml
@@ -1,4 +1,2 @@
[toolchain]
-channel = "1.88.0"
-targets = ["wasm32-unknown-unknown"]
-profile = "default"
\ No newline at end of file
+targets = ["wasm32-unknown-unknown"]
\ No newline at end of file