diff --git a/demos/test-app/dfx.json b/demos/test-app/dfx.example.json
similarity index 100%
rename from demos/test-app/dfx.json
rename to demos/test-app/dfx.example.json
diff --git a/demos/test-app/package.json b/demos/test-app/package.json
index a757faaed3..8f0a89fa4f 100644
--- a/demos/test-app/package.json
+++ b/demos/test-app/package.json
@@ -10,10 +10,13 @@
"buffer": "^6.0.3"
},
"scripts": {
- "build": "vite build",
- "dev": "vite"
+ "dev": "vite --config ./vite.config.ts",
+ "check": "tsc --noEmit",
+ "watch": "npm run check -- --watch",
+ "build": "npm run check && vite --config ./vite.config.ts build"
},
"devDependencies": {
- "vite": "^4.3.9"
+ "vite": "^4.3.9",
+ "typescript": "5.2.2"
}
}
diff --git a/demos/test-app/src/index.html b/demos/test-app/src/index.html
index 6523e657f5..3c07239f95 100644
--- a/demos/test-app/src/index.html
+++ b/demos/test-app/src/index.html
@@ -1,4 +1,7 @@
+
+
+
-
-
Identity
-
Sign In
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
/.well-known/ii-alternative-origins
-
-
Principal:
-
-
Delegation:
-
-
Expiry (ns from now):
-
-
-
-
-
Contact the IC
-
-
-
-
-
- Protocol Tests
-
-
-
-
-
-
-
-
-
-
- WindowPostMessages:
-
+
+
-
+
+
+
diff --git a/demos/test-app/src/main.js b/demos/test-app/src/index.ts
similarity index 66%
rename from demos/test-app/src/main.js
rename to demos/test-app/src/index.ts
index 10ca844080..a533648c82 100644
--- a/demos/test-app/src/main.js
+++ b/demos/test-app/src/index.ts
@@ -1,3 +1,4 @@
+import type { Identity, SignIdentity } from "@dfinity/agent";
import { Actor, HttpAgent } from "@dfinity/agent";
import { AuthClient } from "@dfinity/auth-client";
import {
@@ -8,38 +9,60 @@ import {
} from "@dfinity/identity";
import { Principal } from "@dfinity/principal";
-const signInBtn = document.getElementById("signinBtn");
-const signOutBtn = document.getElementById("signoutBtn");
-const whoamiBtn = document.getElementById("whoamiBtn");
+import "./main.css";
+
+const signInBtn = document.getElementById("signinBtn") as HTMLButtonElement;
+const signOutBtn = document.getElementById("signoutBtn") as HTMLButtonElement;
+const whoamiBtn = document.getElementById("whoamiBtn") as HTMLButtonElement;
const updateAlternativeOriginsBtn = document.getElementById(
"updateNewAlternativeOrigins"
-);
-const openIiWindowBtn = document.getElementById("openIiWindowBtn");
-const closeIiWindowBtn = document.getElementById("closeIIWindowBtn");
-const invalidDataBtn = document.getElementById("invalidDataBtn");
-const incompleteMessageBtn = document.getElementById("incompleteMessageBtn");
-const validMessageBtn = document.getElementById("validMessageBtn");
-const customMessageEl = document.getElementById("customMessage");
-const customMessageBtn = document.getElementById("customMessageBtn");
-const messagesEl = document.getElementById("messages");
-const hostUrlEl = document.getElementById("hostUrl");
+) as HTMLButtonElement;
+const openIiWindowBtn = document.getElementById(
+ "openIiWindowBtn"
+) as HTMLButtonElement;
+const closeIiWindowBtn = document.getElementById(
+ "closeIIWindowBtn"
+) as HTMLButtonElement;
+const invalidDataBtn = document.getElementById(
+ "invalidDataBtn"
+) as HTMLButtonElement;
+const incompleteMessageBtn = document.getElementById(
+ "incompleteMessageBtn"
+) as HTMLButtonElement;
+const validMessageBtn = document.getElementById(
+ "validMessageBtn"
+) as HTMLButtonElement;
+const customMessageEl = document.getElementById(
+ "customMessage"
+) as HTMLInputElement;
+const customMessageBtn = document.getElementById(
+ "customMessageBtn"
+) as HTMLButtonElement;
+const messagesEl = document.getElementById("messages") as HTMLElement;
+const hostUrlEl = document.getElementById("hostUrl") as HTMLInputElement;
const whoAmIResponseEl = document.getElementById("whoamiResponse");
-const alternativeOriginsEl = document.getElementById("alternativeOrigins");
+const alternativeOriginsEl = document.getElementById(
+ "alternativeOrigins"
+) as HTMLDivElement;
const newAlternativeOriginsEl = document.getElementById(
"newAlternativeOrigins"
-);
-const principalEl = document.getElementById("principal");
-const delegationEl = document.getElementById("delegation");
-const expirationEl = document.getElementById("expiration");
-const iiUrlEl = document.getElementById("iiUrl");
-const maxTimeToLiveEl = document.getElementById("maxTimeToLive");
-const derivationOriginEl = document.getElementById("derivationOrigin");
+) as HTMLInputElement;
+const principalEl = document.getElementById("principal") as HTMLDivElement;
+const delegationEl = document.getElementById("delegation") as HTMLPreElement;
+const expirationEl = document.getElementById("expiration") as HTMLDivElement;
+const iiUrlEl = document.getElementById("iiUrl") as HTMLInputElement;
+const maxTimeToLiveEl = document.getElementById(
+ "maxTimeToLive"
+) as HTMLInputElement;
+const derivationOriginEl = document.getElementById(
+ "derivationOrigin"
+) as HTMLInputElement;
-let authClient;
-let iiProtocolTestWindow;
-let localIdentity;
+let authClient: AuthClient;
+let iiProtocolTestWindow: Window | undefined;
+let localIdentity: SignIdentity;
-const idlFactory = ({ IDL }) => {
+const idlFactory = ({ IDL }: { IDL: any }) => {
const HeaderField = IDL.Tuple(IDL.Text, IDL.Text);
const HttpRequest = IDL.Record({
url: IDL.Text,
@@ -68,8 +91,8 @@ const idlFactory = ({ IDL }) => {
});
};
-const updateDelegationView = (identity) => {
- principalEl.innerText = identity.getPrincipal();
+const updateDelegationView = (identity: Identity) => {
+ principalEl.innerText = identity.getPrincipal().toText();
if (identity instanceof DelegationIdentity) {
delegationEl.innerText = JSON.stringify(
identity.getDelegation().toJSON(),
@@ -82,8 +105,10 @@ const updateDelegationView = (identity) => {
.getDelegation()
.delegations.map((d) => d.delegation.expiration)
.reduce((current, next) => (next < current ? next : current));
- expirationEl.innerText =
- nextExpiration - BigInt(Date.now()) * BigInt(1000_000);
+ expirationEl.innerText = (
+ nextExpiration -
+ BigInt(Date.now()) * BigInt(1000_000)
+ ).toString();
} else {
delegationEl.innerText = "Current identity is not a DelegationIdentity";
expirationEl.innerText = "N/A";
@@ -95,7 +120,7 @@ const updateAlternativeOriginsView = async () => {
alternativeOriginsEl.innerText = await response.text();
};
-function addMessageElement(message, received) {
+function addMessageElement(message: unknown, received: boolean) {
const messageContainer = document.createElement("div");
messageContainer.classList.add("postMessage");
const messageTitle = document.createElement("div");
@@ -120,16 +145,18 @@ window.addEventListener("message", (event) => {
if (event.source === iiProtocolTestWindow) {
addMessageElement(event.data, true);
if (event?.data?.kind === "authorize-client-success") {
- const delegations = event.data.delegations.map((signedDelegation) => {
- return {
- delegation: new Delegation(
- signedDelegation.delegation.pubkey,
- signedDelegation.delegation.expiration,
- signedDelegation.delegation.targets
- ),
- signature: signedDelegation.signature.buffer,
- };
- });
+ const delegations = event.data.delegations.map(
+ (signedDelegation: any) => {
+ return {
+ delegation: new Delegation(
+ signedDelegation.delegation.pubkey,
+ signedDelegation.delegation.expiration,
+ signedDelegation.delegation.targets
+ ),
+ signature: signedDelegation.signature.buffer,
+ };
+ }
+ );
const delegationChain = DelegationChain.fromDelegations(
delegations,
event.data.userPublicKey.buffer
@@ -141,8 +168,9 @@ window.addEventListener("message", (event) => {
}
});
-const readCanisterId = () => {
- return document.querySelector("[data-canister-id]").dataset.canisterId;
+const readCanisterId = (): string => {
+ const canIdEl = document.querySelector("[data-canister-id]") as HTMLElement;
+ return canIdEl.dataset.canisterId!;
};
const init = async () => {
@@ -155,7 +183,7 @@ const init = async () => {
if (BigInt(maxTimeToLiveEl.value) > BigInt(0)) {
authClient.login({
identityProvider: iiUrlEl.value,
- maxTimeToLive: BigInt(maxTimeToLive.value),
+ maxTimeToLive: BigInt(maxTimeToLiveEl.value),
derivationOrigin,
onSuccess: () => updateDelegationView(authClient.getIdentity()),
});
@@ -188,7 +216,7 @@ const init = async () => {
}
};
- invalidDataBtn.onclick = () => {
+ invalidDataBtn!.onclick = () => {
if (!iiProtocolTestWindow) {
alert("Open II tab first");
return;
@@ -248,14 +276,21 @@ const init = async () => {
agent: httpAgent,
canisterId,
});
- const modeSelection = document.querySelector(
- 'input[name="alternativeOriginsMode"]:checked'
+ const modeSelection = (
+ document.querySelector(
+ 'input[name="alternativeOriginsMode"]:checked'
+ ) as HTMLInputElement
).value;
- let mode = { CertifiedContent: null };
+ let mode:
+ | { Redirect: { location: string } }
+ | { CertifiedContent: null }
+ | { UncertifiedContent: null } = { CertifiedContent: null };
if (modeSelection === "uncertified") {
mode = { UncertifiedContent: null };
} else if (modeSelection === "redirect") {
- let location = document.getElementById("redirectLocation").value;
+ let location = (
+ document.getElementById("redirectLocation") as HTMLInputElement
+ ).value;
mode = { Redirect: { location: location } };
}
await actor.update_alternative_origins(newAlternativeOriginsEl.value, mode);
@@ -265,24 +300,24 @@ const init = async () => {
init();
-whoamiBtn.addEventListener("click", async () => {
+whoamiBtn!.addEventListener("click", async () => {
const identity = await authClient.getIdentity();
const canisterId = Principal.fromText(readCanisterId());
const actor = Actor.createActor(idlFactory, {
agent: new HttpAgent({
- host: hostUrlEl.value,
+ host: (hostUrlEl as HTMLInputElement).value,
identity,
}),
canisterId,
});
- whoAmIResponseEl.innerText = "Loading...";
+ whoAmIResponseEl!.innerText = "Loading...";
// Similar to the sample project on dfx new:
actor
.whoami()
- .then((principal) => {
- whoAmIResponseEl.innerText = principal.toText();
+ .then((principal: any) => {
+ whoAmIResponseEl!.innerText = principal.toText();
})
.catch((err) => {
console.error("Failed to fetch whoami", err);
diff --git a/demos/test-app/src/main.css b/demos/test-app/src/main.css
new file mode 100644
index 0000000000..105c3483cc
--- /dev/null
+++ b/demos/test-app/src/main.css
@@ -0,0 +1,92 @@
+/* Minimal CSS to make the issuer app usable */
+
+:root {
+ /* vertical spacing between elements */
+ --stack: 0.8rem;
+
+ /* default text color */
+ --text-color: black;
+ /* less important text color */
+ --text-color-faded: grey;
+}
+
+/* some sensible body defaults */
+body {
+ margin: 0;
+ font-size: 1.6rem;
+ font-family: Helvetica, Arial, Sans-Serif;
+}
+
+/* Make titles centered */
+h1 {
+ text-align: center;
+}
+
+/* Space out section */
+section {
+ margin-top: 2em;
+}
+
+/* Center the app and prevent growing too wide */
+main {
+ margin: auto;
+ max-width: 30rem;
+}
+
+label {
+ /* set a less intense color for labels and revert for elements within */
+ color: var(--text-color-faded);
+ font-size: 1rem;
+ margin-top: var(--stack);
+}
+label * {
+ color: var(--text-color);
+}
+
+output {
+ display: block;
+ min-width: 100%;
+ font-size: 1.2rem;
+}
+
+/* Something like "disabled" but for outputs */
+output[data-unset] {
+ color: var(--text-color-faded);
+}
+
+input {
+ /* Prevent padding from growing the element */
+ box-sizing: border-box;
+
+ min-width: 100%;
+ font-size: 1.2rem;
+ border: 0;
+ border-bottom: 1px solid black;
+}
+
+button {
+ display: block;
+ min-width: 100%;
+ height: 3em;
+ margin-top: var(--stack);
+}
+
+/* Some specific styles */
+
+/* Give canister logs a CRT look */
+[data-role="canister-logs"] {
+ /* Prevent padding from growing the element */
+ box-sizing: border-box;
+ padding: 1.5em;
+ font-family: monospace;
+ font-size: 0.8rem;
+ border-radius: 5px;
+
+ background-color: black;
+ color: aquamarine;
+}
+
+/* Add a message on no logs */
+[data-role="canister-logs"][data-unset]::after {
+ content: "Nothing yet";
+}
diff --git a/demos/test-app/tsconfig.json b/demos/test-app/tsconfig.json
new file mode 100644
index 0000000000..a8a0af46cc
--- /dev/null
+++ b/demos/test-app/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "include": ["vite.config.ts", "./src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "lib": ["ES2021", "DOM"],
+ "module": "esnext",
+ "strict": true,
+ "target": "es2018",
+ "skipLibCheck": true,
+ "allowSyntheticDefaultImports": true,
+ "noFallthroughCasesInSwitch": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "types": ["node", "vite/client"]
+ }
+}
diff --git a/demos/test-app/vite.config.ts b/demos/test-app/vite.config.ts
index f70dab8675..26ad1ad5f4 100644
--- a/demos/test-app/vite.config.ts
+++ b/demos/test-app/vite.config.ts
@@ -1,28 +1,39 @@
-import { readFileSync } from "fs";
-import { join } from "path";
-import { defineConfig, type UserConfig } from "vite";
+import { execSync } from "child_process";
+import { defineConfig } from "vite";
-const replicaHost = "http://127.0.0.1:4943" as const;
+/**
+ * Read a canister ID from dfx's local state
+ */
+export const readCanisterId = ({
+ canisterName,
+}: {
+ canisterName: string;
+}): string => {
+ const command = `dfx canister id ${canisterName}`;
+ try {
+ const stdout = execSync(command);
+ return stdout.toString().trim();
+ } catch (e) {
+ throw Error(
+ `Could not get canister ID for '${canisterName}' with command '${command}', was the canister deployed? ${e}`
+ );
+ }
+};
-const rewriteRoute = (pathAndParams: string): string => {
- const readCanisterId = (): string => {
- const canisterIdsJson = join(
- process.cwd(),
- ".dfx",
- "local",
- "canister_ids.json"
+export const getReplicaHost = (): string => {
+ const command = `dfx info webserver-port`;
+ try {
+ const stdout = execSync(command);
+ const port = stdout.toString().trim();
+ return `http://127.0.0.1:${port}`;
+ } catch (e) {
+ throw Error(
+ `Could not get replica port '${command}', is the replica running? ${e}`
);
- try {
- const buffer = readFileSync(canisterIdsJson);
- const {
- test_app: { local },
- } = JSON.parse(buffer.toString("utf-8"));
- return local;
- } catch (e: unknown) {
- throw Error(`Could get canister ID from ${canisterIdsJson}: ${e}`);
- }
- };
+ }
+};
+const rewriteRoute = (pathAndParams: string): string => {
let queryParamsString = `?`;
const [path, params] = pathAndParams.split("?");
@@ -31,55 +42,50 @@ const rewriteRoute = (pathAndParams: string): string => {
queryParamsString += `${params}&`;
}
- queryParamsString += `canisterId=${readCanisterId()}`;
+ queryParamsString += `canisterId=${readCanisterId({
+ canisterName: "test_app",
+ })}`;
return path + queryParamsString;
};
-export default defineConfig(
- ({ mode }: UserConfig): UserConfig => ({
- root: "src",
- build: {
- outDir: "../dist",
- emptyOutDir: true,
- rollupOptions: {
- output: {
- entryFileNames: `[name].js`,
- chunkFileNames: `[name].js`,
- assetFileNames: `[name].[ext]`,
- },
+export default defineConfig(({ command, mode }) => ({
+ root: "./src",
+ build: {
+ outDir: "../dist",
+ emptyOutDir: true,
+ rollupOptions: {
+ output: {
+ entryFileNames: `[name].js`,
+ chunkFileNames: `[name].js`,
+ assetFileNames: `[name].[ext]`,
},
},
- optimizeDeps: {
- esbuildOptions: {
- define: {
- global: "globalThis",
- },
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ define: {
+ global: "globalThis",
},
},
- server: {
- port: 8081,
- // Set up a proxy that redirects API calls and /index.html to the
- // replica; the rest we serve from here.
- proxy: {
- "/api": replicaHost,
- "/": {
- target: replicaHost,
- rewrite: rewriteRoute,
- },
- "/index.html": {
- target: replicaHost,
- rewrite: rewriteRoute,
- },
- "/.well-known/ii-alternative-origins": {
- target: replicaHost,
- rewrite: rewriteRoute,
+ },
+ server:
+ command !== "serve"
+ ? undefined
+ : {
+ port: 8081,
+ // Set up a proxy that redirects API calls and /index.html to the
+ // replica; the rest we serve from here.
+ proxy: {
+ "/api": getReplicaHost(),
+ "/.well-known/ii-alternative-origins": {
+ target: getReplicaHost(),
+ rewrite: rewriteRoute,
+ },
+ "/.well-known/evil-alternative-origins": {
+ target: getReplicaHost(),
+ rewrite: rewriteRoute,
+ },
+ },
},
- "/.well-known/evil-alternative-origins": {
- target: replicaHost,
- rewrite: rewriteRoute,
- },
- },
- },
- })
-);
+}));
diff --git a/tsconfig.all.json b/tsconfig.all.json
index 22aedcc54a..73860b0420 100644
--- a/tsconfig.all.json
+++ b/tsconfig.all.json
@@ -1,5 +1,5 @@
{
"extends": "./tsconfig.json",
- "include": ["src", "./*.ts", "demos/test-app"],
+ "include": ["src", "./*.ts"],
"exclude": [ "src/frontend/generated/*" ]
}