@parity/product-sdk-auth 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ import { describe, expect, test } from "vitest";
17
+ import { ss58Encode } from "@parity/product-sdk-address";
18
+ import { seedToAccount } from "@parity/product-sdk-keys";
19
+ import type { UserSession } from "@parity/product-sdk-terminal";
20
+ import { createSessionSigner } from "./sessionSigner.js";
21
+
22
+ const DEV_PHRASE = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
23
+ // Injected product id (the consumer supplies this via ProductAccountRef). Uses
24
+ // the same value playground derives from so the product account is identical.
25
+ const PRODUCT_ID = "playground.dot";
26
+
27
+ // ────────────────────────────────────────────────────────────────────────────
28
+ // Account equivalence — pins the invariant that every flow which references
29
+ // "the user's account" resolves to the *same* SS58: the product account
30
+ // derived at `mnemonic + "/product/{PRODUCT_ID}/0"`.
31
+ //
32
+ // The signer built by `createSessionSigner(session, {productId, derivationIndex})`
33
+ // must SS58-equal the mobile/playground-app derivation
34
+ // `seedToAccount(mnemonic, "/product/{PRODUCT_ID}/0")`. These tests are the
35
+ // regression guard against the pre-fix bug where the signer used the wallet
36
+ // account (`remoteAccount.accountId`) instead of the product account.
37
+ // ────────────────────────────────────────────────────────────────────────────
38
+ describe("session signer account equivalence", () => {
39
+ // Stand-in for the mobile's SSO handshake response: `rootAccountId` is
40
+ // `deriveRootAccount()` on the mobile = the bare-mnemonic keypair pubkey.
41
+ // Other fields aren't read by `createSessionSigner` in the path under test.
42
+ function fakeSession(mnemonic: string): UserSession {
43
+ const root = seedToAccount(mnemonic, "");
44
+ const wallet = seedToAccount(mnemonic, "//SomeWallet"); // user picking a derived account on mobile
45
+ return {
46
+ id: "test",
47
+ localAccount: { accountId: new Uint8Array(32), pin: undefined },
48
+ remoteAccount: {
49
+ accountId: wallet.publicKey,
50
+ publicKey: wallet.publicKey,
51
+ pin: undefined,
52
+ },
53
+ rootAccountId: root.publicKey,
54
+ } as unknown as UserSession;
55
+ }
56
+
57
+ test("session signer address === product-account derivation address", () => {
58
+ const session = fakeSession(DEV_PHRASE);
59
+
60
+ const cliSigner = createSessionSigner(session, {
61
+ productId: PRODUCT_ID,
62
+ derivationIndex: 0,
63
+ });
64
+ const cliAddress = ss58Encode(cliSigner.publicKey);
65
+
66
+ const mobileDerived = seedToAccount(DEV_PHRASE, `/product/${PRODUCT_ID}/0`);
67
+ const productAccountAddress = ss58Encode(mobileDerived.publicKey);
68
+
69
+ expect(cliAddress).toEqual(productAccountAddress);
70
+ });
71
+
72
+ test("regression: signer does NOT use remoteAccount.accountId (= wallet account)", () => {
73
+ const session = fakeSession(DEV_PHRASE);
74
+ const cliSigner = createSessionSigner(session, {
75
+ productId: PRODUCT_ID,
76
+ derivationIndex: 0,
77
+ });
78
+ const cliAddress = ss58Encode(cliSigner.publicKey);
79
+ const walletAddress = ss58Encode(new Uint8Array(session.remoteAccount.accountId));
80
+
81
+ // Pre-fix bug: signer.publicKey was set from session.remoteAccount.accountId
82
+ // (the user's wallet account), not the product-derived account. The wallet
83
+ // account is what the chain would see as From — different from the funded /
84
+ // allowance-granted product account. This guard ensures we never slip back.
85
+ expect(cliAddress).not.toEqual(walletAddress);
86
+ });
87
+
88
+ test("reports stale sessions without a root account public key", () => {
89
+ const session = {
90
+ ...fakeSession(DEV_PHRASE),
91
+ rootAccountId: new Uint8Array(),
92
+ } as UserSession;
93
+
94
+ expect(() =>
95
+ createSessionSigner(session, {
96
+ productId: PRODUCT_ID,
97
+ derivationIndex: 0,
98
+ }),
99
+ ).toThrow("Stored login session is missing the root account public key.");
100
+ });
101
+ });
@@ -0,0 +1,31 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ /**
17
+ * Session-backed signing for a product account.
18
+ *
19
+ * All of this lives in `@parity/product-sdk-terminal` — the session signer, the
20
+ * product-account derivation, and the stale-session guard. This module is a
21
+ * thin re-export so auth consumers get one import surface; nothing is
22
+ * implemented twice.
23
+ */
24
+
25
+ export {
26
+ createSessionSignerForAccount as createSessionSigner,
27
+ deriveProductPublicKey,
28
+ sessionRootPublicKey,
29
+ INCOMPLETE_SESSION_MESSAGE,
30
+ } from "@parity/product-sdk-terminal";
31
+ export type { ProductAccountRef } from "@parity/product-sdk-terminal";
@@ -0,0 +1,97 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ import { describe, expect, test } from "vitest";
17
+ import { ss58Encode } from "@parity/product-sdk-address";
18
+ import { getDevPublicKey } from "@parity/product-sdk-tx";
19
+ import { seedToAccount } from "@parity/product-sdk-keys";
20
+ import { resolveSigner, SignerNotAvailableError, parseDevAccountName } from "./signer.js";
21
+ import type { AuthClient } from "./auth.js";
22
+
23
+ const DEV_PHRASE = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
24
+
25
+ // Minimal AuthClient stub — only `getSessionSigner` is exercised by resolveSigner.
26
+ function stubAuthClient(session: Awaited<ReturnType<AuthClient["getSessionSigner"]>>): AuthClient {
27
+ return {
28
+ getSessionSigner: async () => session,
29
+ } as unknown as AuthClient;
30
+ }
31
+
32
+ describe("parseDevAccountName", () => {
33
+ test("recognizes well-known dev SURIs (exact case, // prefix)", () => {
34
+ expect(parseDevAccountName("//Alice")).toBe("Alice");
35
+ expect(parseDevAccountName("//Bob")).toBe("Bob");
36
+ });
37
+ test("requires the // prefix — a bare name is not a dev SURI", () => {
38
+ expect(parseDevAccountName("Alice")).toBeNull();
39
+ });
40
+ test("is case-sensitive, matching Substrate SURI semantics", () => {
41
+ expect(parseDevAccountName("//alice")).toBeNull();
42
+ expect(parseDevAccountName("//BOB")).toBeNull();
43
+ });
44
+ test("returns null for non-dev suris", () => {
45
+ expect(parseDevAccountName("//NotADev")).toBeNull();
46
+ expect(parseDevAccountName(DEV_PHRASE)).toBeNull();
47
+ });
48
+ });
49
+
50
+ describe("resolveSigner", () => {
51
+ const noSession = stubAuthClient(null);
52
+
53
+ test("--suri //Alice → dev signer at Alice's address", async () => {
54
+ const r = await resolveSigner(noSession, { suri: "//Alice" });
55
+ expect(r.source).toBe("dev");
56
+ expect(r.address).toBe(ss58Encode(getDevPublicKey("Alice")));
57
+ });
58
+
59
+ test("--suri <mnemonic> → dev signer at the seed-derived address", async () => {
60
+ const r = await resolveSigner(noSession, { suri: DEV_PHRASE });
61
+ expect(r.source).toBe("dev");
62
+ expect(r.address).toBe(ss58Encode(seedToAccount(DEV_PHRASE, "").publicKey));
63
+ });
64
+
65
+ test("--suri <mnemonic>//path → dev signer at the derived sub-account", async () => {
66
+ const r = await resolveSigner(noSession, { suri: `${DEV_PHRASE}//stash` });
67
+ expect(r.source).toBe("dev");
68
+ expect(r.address).toBe(ss58Encode(seedToAccount(DEV_PHRASE, "//stash").publicKey));
69
+ });
70
+
71
+ test("garbage suri → throws 'Unrecognized SURI'", async () => {
72
+ await expect(resolveSigner(noSession, { suri: "not a valid suri !!" })).rejects.toThrow(
73
+ /Unrecognized SURI/,
74
+ );
75
+ });
76
+
77
+ test("no suri + no session → SignerNotAvailableError", async () => {
78
+ await expect(resolveSigner(noSession)).rejects.toBeInstanceOf(SignerNotAvailableError);
79
+ });
80
+
81
+ test("no suri + a session → session signer (source 'session')", async () => {
82
+ const fakeHandle = {
83
+ address: "5SESSION",
84
+ addresses: {
85
+ rootAddress: "r",
86
+ productAddress: "5SESSION",
87
+ productH160: "0xabc" as `0x${string}`,
88
+ },
89
+ signer: {} as never,
90
+ userSession: {} as never,
91
+ destroy() {},
92
+ };
93
+ const r = await resolveSigner(stubAuthClient(fakeHandle));
94
+ expect(r.source).toBe("session");
95
+ expect(r.address).toBe("5SESSION");
96
+ });
97
+ });
package/src/signer.ts ADDED
@@ -0,0 +1,124 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ /**
17
+ * Unified signer resolution — "give me a PolkadotSigner, I don't care how."
18
+ * Lifted from playground-cli `src/utils/signer.ts` (issue #411).
19
+ *
20
+ * Dev accounts (`--suri //Alice`) and QR mobile sessions both produce the same
21
+ * `PolkadotSigner` interface. Consumers call `resolveSigner(authClient)` once at
22
+ * startup and thread the result through all operations. The `authClient` (from
23
+ * `createAuthClient`) supplies the session branch, so this module stays free of
24
+ * product-specific config.
25
+ */
26
+
27
+ import { ss58Encode } from "@parity/product-sdk-address";
28
+ import { createDevSigner, getDevPublicKey, type DevAccountName } from "@parity/product-sdk-tx";
29
+ import { seedToAccount } from "@parity/product-sdk-keys";
30
+ import type { PolkadotSigner } from "polkadot-api";
31
+ import type { AuthClient, SessionAddresses, SessionHandle } from "./auth.js";
32
+
33
+ export type SignerSource = "dev" | "session";
34
+
35
+ export interface ResolvedSigner {
36
+ signer: PolkadotSigner;
37
+ address: string;
38
+ source: SignerSource;
39
+ /** Present for QR/mobile sessions; absent for local dev/SURI signers. */
40
+ userSession?: SessionHandle["userSession"];
41
+ /** Root / product / H160 triple — present for QR/mobile sessions only. */
42
+ addresses?: SessionAddresses;
43
+ /** Tear down session adapter. Call in a finally block. No-op for dev signers. */
44
+ destroy(): void;
45
+ }
46
+
47
+ export interface SignerOptions {
48
+ /** Secret URI like "//Alice". Takes priority over session. */
49
+ suri?: string;
50
+ }
51
+
52
+ export class SignerNotAvailableError extends Error {
53
+ constructor() {
54
+ super('No signer available. Run "login" to sign in, or pass --suri //Alice for dev.');
55
+ this.name = "SignerNotAvailableError";
56
+ }
57
+ }
58
+
59
+ const DEV_NAMES: readonly DevAccountName[] = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie"];
60
+
61
+ /**
62
+ * Parse a well-known dev-account SURI (`//Alice`, `//Bob`, …).
63
+ *
64
+ * Requires the `//` hard-derivation prefix and an exact-case name, matching
65
+ * Substrate SURI semantics (and `createDevSigner`, which derives `//${name}`).
66
+ * A bare `Alice` or a different case is NOT a dev SURI — it falls through to the
67
+ * mnemonic path in `resolveSigner`, where it fails as an invalid mnemonic.
68
+ */
69
+ export function parseDevAccountName(suri: string): DevAccountName | null {
70
+ if (!suri.startsWith("//")) return null;
71
+ const name = suri.slice(2);
72
+ return DEV_NAMES.find((n) => n === name) ?? null;
73
+ }
74
+
75
+ /**
76
+ * Resolve a PolkadotSigner.
77
+ *
78
+ * 1. `--suri` flag → dev signer (dev name like //Alice, OR a BIP-39 mnemonic
79
+ * with optional `//<path>` derivation suffix)
80
+ * 2. Persisted QR session → session signer (via `authClient.getSessionSigner`)
81
+ * 3. Neither → `SignerNotAvailableError`
82
+ */
83
+ export async function resolveSigner(
84
+ authClient: AuthClient,
85
+ options?: SignerOptions,
86
+ ): Promise<ResolvedSigner> {
87
+ if (options?.suri) {
88
+ const devName = parseDevAccountName(options.suri);
89
+ if (devName) {
90
+ return {
91
+ signer: createDevSigner(devName),
92
+ address: ss58Encode(getDevPublicKey(devName)),
93
+ source: "dev",
94
+ destroy() {},
95
+ };
96
+ }
97
+ // Mnemonic path. Split off an optional `//<path>` derivation suffix so
98
+ // callers can target a sub-account of the same seed without a separate flag.
99
+ const sepIdx = options.suri.indexOf("//");
100
+ const mnemonic = (sepIdx === -1 ? options.suri : options.suri.slice(0, sepIdx)).trim();
101
+ const path = sepIdx === -1 ? "" : options.suri.slice(sepIdx);
102
+ try {
103
+ const account = seedToAccount(mnemonic, path);
104
+ return {
105
+ signer: account.signer,
106
+ address: ss58Encode(account.publicKey),
107
+ source: "dev",
108
+ destroy() {},
109
+ };
110
+ } catch {
111
+ throw new Error(
112
+ `Unrecognized SURI "${options.suri}". ` +
113
+ `Expected a dev name (${DEV_NAMES.join(", ")}) or a BIP-39 mnemonic.`,
114
+ );
115
+ }
116
+ }
117
+
118
+ const session = await authClient.getSessionSigner();
119
+ if (session) {
120
+ return { ...session, source: "session" };
121
+ }
122
+
123
+ throw new SignerNotAvailableError();
124
+ }
package/src/types.ts ADDED
@@ -0,0 +1,34 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ /**
17
+ * Per-product configuration injected into `createAuthClient`. Lifting the
18
+ * sign-in glue out of playground-cli (issue #411) means the env-specific
19
+ * constants playground hard-coded in its `config.ts` (DAPP_ID, product id,
20
+ * People-chain endpoints) become consumer-supplied so the same package serves
21
+ * `playground` and `dot` (and future products) unchanged.
22
+ */
23
+ export interface AuthConfig {
24
+ /** The dApp identity string. Scopes the on-disk session namespace
25
+ * (`~/.polkadot-apps/${dappId}_*`) and the SSO pairing — each product
26
+ * gets its own, independently-revocable session. */
27
+ dappId: string;
28
+ /** Product id used to derive the product account (`/product/{productId}/{index}`). */
29
+ productId: string;
30
+ /** Derivation index of the product account (0 = default). */
31
+ derivationIndex: number;
32
+ /** People-parachain RPC endpoints the terminal adapter connects to. */
33
+ peopleEndpoints: string[];
34
+ }
@@ -0,0 +1,22 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ /**
17
+ * `@parity/product-sdk-auth/ui` — terminal UI helpers (QR rendering + login/
18
+ * logout status formatters). Separate entrypoint so headless consumers can
19
+ * import the package root without pulling terminal-rendering deps.
20
+ */
21
+ export { renderQrCode } from "./qr.js";
22
+ export { renderLoginStatus, renderLogoutStatus } from "./status.js";
package/src/ui/qr.ts ADDED
@@ -0,0 +1,22 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ /**
17
+ * QR rendering for the terminal. Re-exported from `@parity/product-sdk-terminal`
18
+ * so consumers get the login QR without depending on the terminal package
19
+ * directly. Lives under the `./ui` entrypoint (pulls terminal-rendering deps);
20
+ * headless consumers import the package root instead.
21
+ */
22
+ export { renderQrCode } from "@parity/product-sdk-terminal";
@@ -0,0 +1,60 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ import { describe, expect, test } from "vitest";
17
+ import { renderLoginStatus, renderLogoutStatus } from "./status.js";
18
+
19
+ describe("renderLoginStatus", () => {
20
+ test("waiting", () => {
21
+ expect(renderLoginStatus({ step: "waiting" })).toMatch(/scan/i);
22
+ });
23
+ test("paired", () => {
24
+ expect(renderLoginStatus({ step: "paired" })).toMatch(/finishing/i);
25
+ });
26
+ test("pending surfaces the stage", () => {
27
+ expect(renderLoginStatus({ step: "pending", stage: "attestation" })).toContain(
28
+ "attestation",
29
+ );
30
+ });
31
+ test("success surfaces the address", () => {
32
+ expect(
33
+ renderLoginStatus({
34
+ step: "success",
35
+ address: "5Fy2sypq",
36
+ addresses: { rootAddress: "r", productAddress: "5Fy2sypq", productH160: "0xabc" },
37
+ }),
38
+ ).toContain("5Fy2sypq");
39
+ });
40
+ test("error surfaces the message", () => {
41
+ expect(renderLoginStatus({ step: "error", message: "boom" })).toContain("boom");
42
+ });
43
+ });
44
+
45
+ describe("renderLogoutStatus", () => {
46
+ test("disconnecting", () => {
47
+ expect(renderLogoutStatus({ step: "disconnecting", address: "5X" })).toContain("5X");
48
+ });
49
+ test("success", () => {
50
+ expect(renderLogoutStatus({ step: "success", address: "5X" })).toMatch(/signed out/i);
51
+ });
52
+ test("partial surfaces the reason", () => {
53
+ expect(renderLogoutStatus({ step: "partial", address: "5X", reason: "ws down" })).toContain(
54
+ "ws down",
55
+ );
56
+ });
57
+ test("error surfaces the message", () => {
58
+ expect(renderLogoutStatus({ step: "error", message: "nope" })).toContain("nope");
59
+ });
60
+ });
@@ -0,0 +1,49 @@
1
+ // Copyright (C) Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+
16
+ /**
17
+ * Pure formatters that map the `LoginStatus` / `LogoutStatus` streams to
18
+ * one-line terminal messages. No I/O, no terminal deps — `import type` only,
19
+ * so this module stays unit-testable without the SSO stack.
20
+ */
21
+ import type { LoginStatus, LogoutStatus } from "../auth.js";
22
+
23
+ export function renderLoginStatus(status: LoginStatus): string {
24
+ switch (status.step) {
25
+ case "waiting":
26
+ return "Waiting for you to scan the QR code with your phone…";
27
+ case "paired":
28
+ return "Paired — finishing sign-in…";
29
+ case "pending":
30
+ return `Working: ${status.stage}…`;
31
+ case "success":
32
+ return `Signed in as ${status.address}`;
33
+ case "error":
34
+ return `Sign-in failed: ${status.message}`;
35
+ }
36
+ }
37
+
38
+ export function renderLogoutStatus(status: LogoutStatus): string {
39
+ switch (status.step) {
40
+ case "disconnecting":
41
+ return `Signing out ${status.address}…`;
42
+ case "success":
43
+ return `Signed out ${status.address}`;
44
+ case "partial":
45
+ return `Signed out locally (${status.address}); the phone was not notified: ${status.reason}`;
46
+ case "error":
47
+ return `Sign-out error: ${status.message}`;
48
+ }
49
+ }