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:

-
+
+
+

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/*" ] }