@parity/product-sdk-auth 0.0.0-dev.312.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,107 @@
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
+ * RFC-0010 `host_request_resource_allocation` helpers for auth consumers.
18
+ *
19
+ * The wire call, and the resource / outcome types, live in
20
+ * `@parity/product-sdk-terminal` (derived from `UserSession` so they can't
21
+ * drift from the host codec). This module delegates the wire call to terminal's
22
+ * `sendResourceAllocation` — the cache-free primitive — and layers on the
23
+ * auth-consumer conveniences (`DEFAULT_RESOURCES`, `summarizeOutcomes`) plus a
24
+ * `productId`-based signature so headless callers don't need a `TerminalAdapter`.
25
+ *
26
+ * The mobile app handles `hostRequestResourceAllocation` in
27
+ * `AllowanceHostCalls.kt` and routes the user through an approval UI.
28
+ */
29
+
30
+ import type { UserSession } from "@parity/product-sdk-terminal";
31
+ import {
32
+ type AllocatableResource,
33
+ type ApAllocationOutcome,
34
+ type OnExistingAllowancePolicy,
35
+ sendResourceAllocation,
36
+ } from "@parity/product-sdk-terminal";
37
+
38
+ export type { AllocatableResource, OnExistingAllowancePolicy } from "@parity/product-sdk-terminal";
39
+
40
+ /**
41
+ * Outcome of one allocation. Re-exported from terminal's wire-derived type
42
+ * under the name auth consumers already use. We don't read the inner
43
+ * `Allocated` payload (allowance slot keys, derivation secrets) — the host
44
+ * stores them and uses them transparently on subsequent calls. We just need
45
+ * the tag to know whether the allocation succeeded.
46
+ */
47
+ export type AllocationOutcome = ApAllocationOutcome;
48
+
49
+ /** Tag-only view, handy for downstream code that doesn't care about payloads. */
50
+ export type ResourceTag = AllocatableResource["tag"];
51
+
52
+ /**
53
+ * Default mobile-granted resource set for a CLI product account: write access
54
+ * to the statement store + Bulletin, plus PGAS sponsoring for the default
55
+ * (index 0) product account.
56
+ */
57
+ export const DEFAULT_RESOURCES: AllocatableResource[] = [
58
+ { tag: "BulletInAllowance", value: undefined },
59
+ { tag: "StatementStoreAllowance", value: undefined },
60
+ // derivation index 0 = the default product account.
61
+ { tag: "SmartContractAllowance", value: 0 },
62
+ ];
63
+
64
+ /**
65
+ * Send a `host_request_resource_allocation` request over the user's active
66
+ * session. The host (mobile wallet) prompts the user to approve and returns
67
+ * one outcome per requested resource in order.
68
+ *
69
+ * Throws on transport-level failures (Statement Store unreachable, encryption
70
+ * error, etc.). Per-resource refusals are reported as `Rejected`/`NotAvailable`
71
+ * outcomes — callers inspect the array to decide whether to proceed.
72
+ */
73
+ export async function requestResourceAllocation(
74
+ session: UserSession,
75
+ productId: string,
76
+ resources: AllocatableResource[] = DEFAULT_RESOURCES,
77
+ onExisting: OnExistingAllowancePolicy = "Ignore",
78
+ ): Promise<AllocationOutcome[]> {
79
+ return sendResourceAllocation(session, productId, resources, onExisting);
80
+ }
81
+
82
+ export interface AllocationSummary {
83
+ granted: AllocatableResource[];
84
+ rejected: AllocatableResource[];
85
+ unavailable: AllocatableResource[];
86
+ }
87
+
88
+ /**
89
+ * Bucket allocation outcomes by tag. Order-sensitive: `outcomes[i]` maps to
90
+ * `resources[i]`. Outcomes without a matching resource are silently dropped.
91
+ */
92
+ export function summarizeOutcomes(
93
+ outcomes: AllocationOutcome[],
94
+ resources: AllocatableResource[],
95
+ ): AllocationSummary {
96
+ const granted: AllocatableResource[] = [];
97
+ const rejected: AllocatableResource[] = [];
98
+ const unavailable: AllocatableResource[] = [];
99
+ outcomes.forEach((outcome, i) => {
100
+ const resource = resources[i];
101
+ if (!resource) return;
102
+ if (outcome.tag === "Allocated") granted.push(resource);
103
+ else if (outcome.tag === "Rejected") rejected.push(resource);
104
+ else unavailable.push(resource);
105
+ });
106
+ return { granted, rejected, unavailable };
107
+ }
@@ -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
+ import { describe, expect, test } from "vitest";
17
+ import { createAuthClient } from "./auth.js";
18
+ import type { AuthConfig } from "./types.js";
19
+
20
+ const config: AuthConfig = {
21
+ dappId: "test-app",
22
+ productId: "test.dot",
23
+ derivationIndex: 0,
24
+ peopleEndpoints: ["wss://example.com/people"],
25
+ };
26
+
27
+ describe("createAuthClient", () => {
28
+ test("returns the bound auth surface (all functions)", () => {
29
+ const client = createAuthClient(config);
30
+ for (const fn of [
31
+ "connect",
32
+ "waitForLogin",
33
+ "getSessionSigner",
34
+ "findSession",
35
+ "waitForLogout",
36
+ "requestAllocation",
37
+ "clearLocalAppStorage",
38
+ ] as const) {
39
+ expect(typeof client[fn]).toBe("function");
40
+ }
41
+ });
42
+
43
+ test("clearLocalAppStorage returns (no throw) when the dir does not exist", async () => {
44
+ const client = createAuthClient(config);
45
+ await expect(
46
+ client.clearLocalAppStorage("/nonexistent/dir/should-not-exist-xyz"),
47
+ ).resolves.toBeUndefined();
48
+ });
49
+ });
package/src/auth.ts ADDED
@@ -0,0 +1,486 @@
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 login flow — pure business logic, no UI. Lifted from playground-cli
18
+ * `src/utils/auth.ts` (issue #411); the env-specific config playground imported
19
+ * from its `config.ts` is now injected via `createAuthClient(config)`.
20
+ *
21
+ * Flow:
22
+ * 1. `connect()` — starts adapter + auth, returns existing address OR QR code
23
+ * 2. Print QR code to stdout (if needed) — before any UI mounts
24
+ * 3. `waitForLogin()` — awaits the already-running auth to complete
25
+ * 4. `getSessionSigner()` — gets a working signer for tx signing (separate adapter)
26
+ * 5. `requestAllocation()` — RFC-0010 resource allocation (needed before a fresh session can sign)
27
+ * 6. `findSession()` / `waitForLogout()` — sign out flow, mirror image of connect/waitForLogin
28
+ */
29
+
30
+ import type { Dirent } from "node:fs";
31
+ import { readdir, unlink } from "node:fs/promises";
32
+ import { homedir } from "node:os";
33
+ import { join } from "node:path";
34
+ import { deriveH160, ss58Encode } from "@parity/product-sdk-address";
35
+ import {
36
+ createTerminalAdapter,
37
+ waitForSessions,
38
+ renderQrCode,
39
+ type TerminalAdapter,
40
+ type PairingStatus,
41
+ type UserSession,
42
+ } from "@parity/product-sdk-terminal";
43
+ import type { PolkadotSigner } from "polkadot-api";
44
+ import {
45
+ createSessionSigner,
46
+ deriveProductPublicKey,
47
+ sessionRootPublicKey,
48
+ } from "./sessionSigner.js";
49
+ import {
50
+ requestResourceAllocation,
51
+ DEFAULT_RESOURCES,
52
+ type AllocatableResource,
53
+ type AllocationOutcome,
54
+ type OnExistingAllowancePolicy,
55
+ } from "./allocations.js";
56
+ import type { AuthConfig } from "./types.js";
57
+
58
+ /** How long we wait for the statement store to publish the pairing QR. */
59
+ const QR_TIMEOUT_MS = 60_000;
60
+
61
+ /**
62
+ * The three addresses we surface from a paired session.
63
+ *
64
+ * - `rootAddress` — SS58 of `session.rootAccountId`, the `rootUserAccountId`
65
+ * the mobile app sent over the SSO handshake (bare-mnemonic sr25519 root on
66
+ * current mobile builds). Keyed by `Resources.Consumers` on the People
67
+ * parachain, so it's the right input for `lookupUsername` from
68
+ * `@parity/product-sdk-individuality`, which reads that account's usernames.
69
+ * - `productAddress` — SS58 of the product account derived via
70
+ * `product/{productId}/{index}` from `rootAccountId`. This is what actually
71
+ * signs on-chain transactions from the CLI.
72
+ * - `productH160` — the same product pubkey as a 20-byte EVM address (Revive /
73
+ * contracts view). Derived from the SAME pubkey as `productAddress`.
74
+ */
75
+ export interface SessionAddresses {
76
+ rootAddress: string;
77
+ productAddress: string;
78
+ productH160: `0x${string}`;
79
+ }
80
+
81
+ export type ConnectResult =
82
+ | { kind: "existing"; address: string; addresses: SessionAddresses }
83
+ | { kind: "qr"; qrCode: string; login: LoginHandle };
84
+
85
+ export type LoginStatus =
86
+ | { step: "waiting" }
87
+ | { step: "paired" }
88
+ | { step: "pending"; stage: string }
89
+ | { step: "success"; address: string; addresses: SessionAddresses }
90
+ | { step: "error"; message: string };
91
+
92
+ export interface LoginHandle {
93
+ adapter: TerminalAdapter;
94
+ /** The authenticate() promise — already running since connect(). */
95
+ authPromise: ReturnType<TerminalAdapter["sso"]["authenticate"]>;
96
+ }
97
+
98
+ /**
99
+ * A session signer bundle — the signer plus an explicit `destroy()` that tears
100
+ * down the long-lived adapter the signer depends on. Callers MUST invoke
101
+ * `destroy()` once done — the WebSocket keeps the event loop alive.
102
+ */
103
+ export interface SessionHandle {
104
+ address: string;
105
+ addresses: SessionAddresses;
106
+ signer: PolkadotSigner;
107
+ userSession: UserSession;
108
+ destroy(): void;
109
+ }
110
+
111
+ export type LogoutStatus =
112
+ | { step: "disconnecting"; address: string }
113
+ | { step: "success"; address: string }
114
+ | { step: "partial"; address: string; reason: string }
115
+ | { step: "error"; message: string };
116
+
117
+ export interface LogoutHandle {
118
+ adapter: TerminalAdapter;
119
+ address: string;
120
+ session: UserSession;
121
+ }
122
+
123
+ /** The product-bound auth surface returned by `createAuthClient`. */
124
+ export interface AuthClient {
125
+ connect(): Promise<ConnectResult>;
126
+ waitForLogin(
127
+ handle: LoginHandle,
128
+ onStatus: (status: LoginStatus) => void,
129
+ ): Promise<string | null>;
130
+ getSessionSigner(): Promise<SessionHandle | null>;
131
+ findSession(): Promise<LogoutHandle | null>;
132
+ waitForLogout(handle: LogoutHandle, onStatus: (status: LogoutStatus) => void): Promise<void>;
133
+ requestAllocation(
134
+ session: UserSession,
135
+ resources?: AllocatableResource[],
136
+ onExisting?: OnExistingAllowancePolicy,
137
+ ): Promise<AllocationOutcome[]>;
138
+ clearLocalAppStorage(dir?: string): Promise<void>;
139
+ }
140
+
141
+ /**
142
+ * Build an auth client bound to a product's `AuthConfig`. All adapter creation,
143
+ * address derivation, and session-storage scoping read from `config`, so the
144
+ * same code serves any product.
145
+ */
146
+ export function createAuthClient(config: AuthConfig): AuthClient {
147
+ const ref = { productId: config.productId, derivationIndex: config.derivationIndex };
148
+
149
+ function createAdapter(): TerminalAdapter {
150
+ return createTerminalAdapter({
151
+ appId: config.dappId,
152
+ endpoints: config.peopleEndpoints,
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Compute the three display addresses from a paired session. Shares
158
+ * `deriveProductPublicKey` with `createSessionSigner` so the signing key and
159
+ * the display SS58/H160 are computed by exactly one function.
160
+ */
161
+ function deriveSessionAddresses(session: UserSession): SessionAddresses {
162
+ const rootBytes = sessionRootPublicKey(session);
163
+ const productPubkey = deriveProductPublicKey(session, ref);
164
+ return {
165
+ rootAddress: ss58Encode(rootBytes),
166
+ productAddress: ss58Encode(productPubkey),
167
+ productH160: deriveH160(productPubkey),
168
+ };
169
+ }
170
+
171
+ function createSigner(session: UserSession): PolkadotSigner {
172
+ return createSessionSigner(session, ref);
173
+ }
174
+
175
+ function sessionRemoteAddress(session: UserSession): string | null {
176
+ const raw = (session as { remoteAccount?: { accountId?: Uint8Array } }).remoteAccount
177
+ ?.accountId;
178
+ const accountId = raw ? new Uint8Array(raw) : new Uint8Array();
179
+ return accountId.length === 32 ? ss58Encode(accountId) : null;
180
+ }
181
+
182
+ function sessionLogoutAddress(session: UserSession): string {
183
+ try {
184
+ return deriveSessionAddresses(session).productAddress;
185
+ } catch {
186
+ return sessionRemoteAddress(session) ?? "(stored session)";
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Connect to the statement store and resolve the login state. Returns
192
+ * immediately if an existing session is found (address only). Otherwise
193
+ * kicks off authenticate(), waits for the QR payload, and returns the QR
194
+ * code + a handle to await the auth result.
195
+ */
196
+ async function connect(): Promise<ConnectResult> {
197
+ const adapter = createAdapter();
198
+
199
+ const sessions = await waitForSessions(adapter);
200
+ if (sessions.length > 0) {
201
+ // The pairing adapter isn't handed back on the existing-session path
202
+ // (ConnectResult.existing carries no handle), so tear it down before
203
+ // returning — including if deriveSessionAddresses throws on a stale
204
+ // session — or its WebSocket leaks for the process lifetime.
205
+ try {
206
+ const addresses = deriveSessionAddresses(sessions[0]);
207
+ return { kind: "existing", address: addresses.productAddress, addresses };
208
+ } finally {
209
+ adapter.destroy().catch(() => {});
210
+ }
211
+ }
212
+
213
+ // Start authenticate — this triggers the pairing flow and QR emission.
214
+ const authPromise = adapter.sso.authenticate();
215
+
216
+ // Captured so the loser of the race can be cancelled: if the QR payload
217
+ // wins, an un-cleared setTimeout stays armed and keeps the Node event
218
+ // loop alive, hanging the CLI for up to QR_TIMEOUT_MS after login.
219
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
220
+ try {
221
+ const qrCode = await Promise.race([
222
+ new Promise<string>((resolve) => {
223
+ let done = false;
224
+ // biome-ignore lint/style/useConst: self-referential — the callback calls unsub() to unsubscribe itself, so it can't be const.
225
+ let unsub: (() => void) | undefined;
226
+ unsub = adapter.sso.pairingStatus.subscribe(async (status: PairingStatus) => {
227
+ if (status.step === "pairing" && !done) {
228
+ done = true;
229
+ unsub?.();
230
+ resolve(await renderQrCode(status.payload));
231
+ }
232
+ });
233
+ }),
234
+ new Promise<never>((_, reject) => {
235
+ timeoutHandle = setTimeout(
236
+ () =>
237
+ reject(
238
+ new Error(
239
+ `Login service did not respond within ${Math.round(
240
+ QR_TIMEOUT_MS / 1000,
241
+ )}s — try again`,
242
+ ),
243
+ ),
244
+ QR_TIMEOUT_MS,
245
+ );
246
+ }),
247
+ ]);
248
+
249
+ return { kind: "qr", qrCode, login: { adapter, authPromise } };
250
+ } catch (err) {
251
+ // Release the WebSocket so we don't leak on the error path. `.catch()`
252
+ // swallows the post-destroy `DestroyedError` that polkadot-api's
253
+ // raw-client surfaces when a pending chainHead unsubscribe races the
254
+ // WS close.
255
+ adapter.destroy().catch(() => {});
256
+ throw err;
257
+ } finally {
258
+ // Clear the timeout regardless of which promise won — on the QR-win
259
+ // path this disarms the still-pending timer; on the timeout/error
260
+ // path it's a harmless no-op.
261
+ clearTimeout(timeoutHandle);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Wait for the already-running login to complete. Call after the QR code is
267
+ * displayed. Reports status via callback. Returns the SS58 address on
268
+ * success, or null on failure.
269
+ */
270
+ async function waitForLogin(
271
+ { adapter, authPromise }: LoginHandle,
272
+ onStatus: (status: LoginStatus) => void,
273
+ ): Promise<string | null> {
274
+ onStatus({ step: "waiting" });
275
+
276
+ const unsubPairing = adapter.sso.pairingStatus.subscribe((status: PairingStatus) => {
277
+ if (status.step === "finished") {
278
+ onStatus({ step: "paired" });
279
+ } else if (status.step === "pending") {
280
+ onStatus({ step: "pending", stage: status.stage });
281
+ } else if (status.step === "pairingError") {
282
+ onStatus({ step: "error", message: status.message });
283
+ }
284
+ });
285
+
286
+ let authenticated = false;
287
+ let address: string | null = null;
288
+ try {
289
+ const result = await authPromise;
290
+ result.match(
291
+ (session) => {
292
+ if (session) {
293
+ authenticated = true;
294
+ } else {
295
+ // Ok channel but no session — pairing resolved without
296
+ // producing a session. Surface it rather than returning
297
+ // null with no status, which would hang the caller.
298
+ onStatus({
299
+ step: "error",
300
+ message: "Login did not produce a session",
301
+ });
302
+ }
303
+ },
304
+ (error) => {
305
+ onStatus({ step: "error", message: error.message });
306
+ },
307
+ );
308
+ if (authenticated) {
309
+ const sessions = await waitForSessions(adapter, 3000);
310
+ if (sessions.length > 0) {
311
+ const addresses = deriveSessionAddresses(sessions[0]);
312
+ address = addresses.productAddress;
313
+ onStatus({ step: "success", address, addresses });
314
+ } else {
315
+ onStatus({
316
+ step: "error",
317
+ message: "Login succeeded but the local session was not available",
318
+ });
319
+ }
320
+ }
321
+ } finally {
322
+ unsubPairing();
323
+ // The pairing adapter isn't needed after login — getSessionSigner()
324
+ // builds its own — so tear it down to release the WebSocket.
325
+ adapter.destroy().catch(() => {});
326
+ }
327
+
328
+ return address;
329
+ }
330
+
331
+ /**
332
+ * Get a working signer from a persisted session. The returned handle owns a
333
+ * terminal adapter that stays alive while the signer is in use (signing goes
334
+ * through the adapter's WebSocket). Call `destroy()` when done. Returns null
335
+ * if no session exists.
336
+ */
337
+ async function getSessionSigner(): Promise<SessionHandle | null> {
338
+ const adapter = createAdapter();
339
+
340
+ const sessions = await waitForSessions(adapter, 3000);
341
+ if (sessions.length === 0) {
342
+ adapter.destroy().catch(() => {});
343
+ return null;
344
+ }
345
+
346
+ const session = sessions[0];
347
+ // createSigner / deriveSessionAddresses derive the product key and can
348
+ // throw on a stale session (missing rootAccountId). Destroy the adapter
349
+ // on that path — the handle that owns `destroy` is never returned, so
350
+ // without this the WebSocket leaks.
351
+ let signer: PolkadotSigner;
352
+ let addresses: SessionAddresses;
353
+ try {
354
+ signer = createSigner(session);
355
+ addresses = deriveSessionAddresses(session);
356
+ } catch (err) {
357
+ adapter.destroy().catch(() => {});
358
+ throw err;
359
+ }
360
+
361
+ let destroyed = false;
362
+ const destroy = () => {
363
+ if (destroyed) return;
364
+ destroyed = true;
365
+ adapter.destroy().catch(() => {});
366
+ };
367
+
368
+ return {
369
+ address: addresses.productAddress,
370
+ addresses,
371
+ signer,
372
+ userSession: session,
373
+ destroy,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * RFC-0010 resource allocation over the user's active session — grants the
379
+ * statement-store / Bulletin / smart-contract allowances the product account
380
+ * needs before it can sign. The mobile wallet prompts the user to approve.
381
+ */
382
+ async function requestAllocation(
383
+ session: UserSession,
384
+ resources: AllocatableResource[] = DEFAULT_RESOURCES,
385
+ onExisting: OnExistingAllowancePolicy = "Ignore",
386
+ ): Promise<AllocationOutcome[]> {
387
+ return requestResourceAllocation(session, config.productId, resources, onExisting);
388
+ }
389
+
390
+ /**
391
+ * Look up the currently paired session, if any. Returns a handle ready for
392
+ * `waitForLogout()`, or null when no session is signed in (adapter destroyed
393
+ * on the null path).
394
+ */
395
+ async function findSession(): Promise<LogoutHandle | null> {
396
+ const adapter = createAdapter();
397
+ const sessions = await waitForSessions(adapter, 3000);
398
+ if (sessions.length === 0) {
399
+ try {
400
+ await adapter.destroy();
401
+ } catch {
402
+ // best-effort
403
+ }
404
+ return null;
405
+ }
406
+ const session = sessions[0];
407
+ const address = sessionLogoutAddress(session);
408
+ return { adapter, address, session };
409
+ }
410
+
411
+ /**
412
+ * Disconnect the given session. Sends a `Disconnected` statement so the
413
+ * paired mobile app drops its side, then clears the local `${dappId}_*`
414
+ * files. Always releases the adapter before returning.
415
+ */
416
+ async function waitForLogout(
417
+ handle: LogoutHandle,
418
+ onStatus: (status: LogoutStatus) => void,
419
+ ): Promise<void> {
420
+ const { adapter, address, session } = handle;
421
+
422
+ try {
423
+ onStatus({ step: "disconnecting", address });
424
+ const result = await adapter.sessions.disconnect(session);
425
+ if (result.isOk()) {
426
+ await clearLocalAppStorage();
427
+ onStatus({ step: "success", address });
428
+ return;
429
+ }
430
+ const reason = result.error.message || "remote unreachable";
431
+ await clearLocalAppStorage();
432
+ onStatus({ step: "partial", address, reason });
433
+ } catch (err) {
434
+ const reason = err instanceof Error ? err.message : String(err);
435
+ try {
436
+ await clearLocalAppStorage();
437
+ onStatus({ step: "partial", address, reason });
438
+ } catch (cleanupErr) {
439
+ const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
440
+ onStatus({ step: "error", message: msg });
441
+ }
442
+ } finally {
443
+ try {
444
+ await adapter.destroy();
445
+ } catch {
446
+ // best-effort
447
+ }
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Best-effort removal of this app's persisted state under `~/.polkadot-apps/`.
453
+ * Scoped by `${dappId}_` prefix so files belonging to other polkadot apps
454
+ * sharing the directory are left alone. Errors are swallowed.
455
+ */
456
+ async function clearLocalAppStorage(
457
+ dir: string = join(homedir(), ".polkadot-apps"),
458
+ ): Promise<void> {
459
+ let entries: Dirent[];
460
+ try {
461
+ entries = await readdir(dir, { withFileTypes: true });
462
+ } catch {
463
+ return;
464
+ }
465
+ const prefix = `${config.dappId}_`;
466
+ await Promise.all(
467
+ entries
468
+ .filter((entry) => entry.isFile() && entry.name.startsWith(prefix))
469
+ .map((entry) =>
470
+ unlink(join(dir, entry.name)).catch(() => {
471
+ // best-effort
472
+ }),
473
+ ),
474
+ );
475
+ }
476
+
477
+ return {
478
+ connect,
479
+ waitForLogin,
480
+ getSessionSigner,
481
+ findSession,
482
+ waitForLogout,
483
+ requestAllocation,
484
+ clearLocalAppStorage,
485
+ };
486
+ }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
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 — QR/mobile sign-in + session signing for Polkadot
18
+ * product CLIs. Runtime-agnostic core (no terminal UI). UI helpers (QR render,
19
+ * status formatters) live under the `./ui` entrypoint so headless consumers
20
+ * don't pull terminal-rendering.
21
+ */
22
+
23
+ export { createAuthClient } from "./auth.js";
24
+ export type {
25
+ AuthClient,
26
+ SessionAddresses,
27
+ ConnectResult,
28
+ LoginStatus,
29
+ LoginHandle,
30
+ SessionHandle,
31
+ LogoutStatus,
32
+ LogoutHandle,
33
+ } from "./auth.js";
34
+
35
+ export {
36
+ createSessionSigner,
37
+ deriveProductPublicKey,
38
+ sessionRootPublicKey,
39
+ INCOMPLETE_SESSION_MESSAGE,
40
+ } from "./sessionSigner.js";
41
+ export type { ProductAccountRef } from "./sessionSigner.js";
42
+
43
+ export { resolveSigner, SignerNotAvailableError, parseDevAccountName } from "./signer.js";
44
+ export type { ResolvedSigner, SignerSource, SignerOptions } from "./signer.js";
45
+
46
+ export { requestResourceAllocation, summarizeOutcomes, DEFAULT_RESOURCES } from "./allocations.js";
47
+ export type {
48
+ AllocatableResource,
49
+ AllocationOutcome,
50
+ AllocationSummary,
51
+ OnExistingAllowancePolicy,
52
+ ResourceTag,
53
+ } from "./allocations.js";
54
+
55
+ export type { AuthConfig } from "./types.js";