@parity/product-sdk-host 0.8.0 → 0.10.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.
- package/dist/index.d.ts +76 -3
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -15
- package/src/index.ts +8 -0
- package/src/notifications.ts +123 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { JsonRpcProvider } from 'polkadot-api';
|
|
2
2
|
import * as _novasamatech_host_api from '@novasamatech/host-api';
|
|
3
3
|
import { Subscription, Transport, CodecType, AllocatableResource as AllocatableResource$1, AllocationOutcome as AllocationOutcome$1, RemotePermission as RemotePermission$1, DevicePermission } from '@novasamatech/host-api';
|
|
4
|
-
export { HexString, assertEnumVariant, enumValue, fromHex, isEnumVariant, resultErr, resultOk, toHex, unwrapResultOrThrow } from '@novasamatech/host-api';
|
|
4
|
+
export { HexString, PushNotificationError, assertEnumVariant, enumValue, fromHex, isEnumVariant, resultErr, resultOk, toHex, unwrapResultOrThrow } from '@novasamatech/host-api';
|
|
5
5
|
import * as _novasamatech_host_api_wrapper from '@novasamatech/host-api-wrapper';
|
|
6
|
-
import { hostLocalStorage, createStatementStore, ProductAccountId as ProductAccountId$1, SignedStatement as SignedStatement$1, Statement as Statement$1, StatementTopicFilter as StatementTopicFilter$1, StatementsPage as StatementsPage$1, Topic as Topic$1, createAccountsProvider, preimageManager, ThemeMode as ThemeMode$1, createThemeProvider, ChatBotRegistrationResult as ChatBotRegistrationResult$1, ChatCustomMessageRenderer as ChatCustomMessageRenderer$1, ChatCustomMessageRendererParams as ChatCustomMessageRendererParams$1, createProductChatManager, ChatMessageContent as ChatMessageContent$1, ChatReceivedAction as ChatReceivedAction$1, ChatRoom as ChatRoom$1, ChatRoomRegistrationResult as ChatRoomRegistrationResult$1, PaymentBalance as PaymentBalance$1, paymentManager, PaymentStatus as PaymentStatus$1, TopUpSource as TopUpSource$1 } from '@novasamatech/host-api-wrapper';
|
|
6
|
+
import { hostLocalStorage, createStatementStore, ProductAccountId as ProductAccountId$1, SignedStatement as SignedStatement$1, Statement as Statement$1, StatementTopicFilter as StatementTopicFilter$1, StatementsPage as StatementsPage$1, Topic as Topic$1, createAccountsProvider, preimageManager, ThemeMode as ThemeMode$1, createThemeProvider, ChatBotRegistrationResult as ChatBotRegistrationResult$1, ChatCustomMessageRenderer as ChatCustomMessageRenderer$1, ChatCustomMessageRendererParams as ChatCustomMessageRendererParams$1, createProductChatManager, ChatMessageContent as ChatMessageContent$1, ChatReceivedAction as ChatReceivedAction$1, ChatRoom as ChatRoom$1, ChatRoomRegistrationResult as ChatRoomRegistrationResult$1, PaymentBalance as PaymentBalance$1, paymentManager, PaymentStatus as PaymentStatus$1, TopUpSource as TopUpSource$1, notificationManager } from '@novasamatech/host-api-wrapper';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Public types for the host wrappers.
|
|
@@ -728,4 +728,77 @@ type PaymentManager = typeof paymentManager;
|
|
|
728
728
|
*/
|
|
729
729
|
declare function getPaymentManager(): Promise<PaymentManager | null>;
|
|
730
730
|
|
|
731
|
-
|
|
731
|
+
/**
|
|
732
|
+
* Wrapper for the host's scheduled push-notification surface.
|
|
733
|
+
*
|
|
734
|
+
* Shipped flat-in-host rather than as `getTruApi().notification.*` because
|
|
735
|
+
* the upstream JS `hostApi` is itself a flat object - there is no
|
|
736
|
+
* `.notification` accessor to mirror. A flat `getNotificationManager()`
|
|
737
|
+
* matches the singleton pattern already used by {@link getPaymentManager},
|
|
738
|
+
* {@link getPreimageManager}, and {@link getHostLocalStorage}.
|
|
739
|
+
*
|
|
740
|
+
* Returns the shared `notificationManager` singleton from
|
|
741
|
+
* `@novasamatech/host-api-wrapper` (not a fresh `createNotificationManager()`
|
|
742
|
+
* instance) so callers share one wrapper + hostApi closure across the app.
|
|
743
|
+
*
|
|
744
|
+
* {@link PushNotificationError} is re-exported from `@novasamatech/host-api`
|
|
745
|
+
* so consumers can branch on `err instanceof
|
|
746
|
+
* PushNotificationError.ScheduleLimitReached` (the host's pending-notification
|
|
747
|
+
* cap) without importing the novasama packages directly.
|
|
748
|
+
*
|
|
749
|
+
* @module
|
|
750
|
+
*/
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Host notification manager handle. Exposes `push(input)` (resolves to a
|
|
754
|
+
* {@link NotificationId}) and `cancel(id)`.
|
|
755
|
+
*
|
|
756
|
+
* Type identical to `notificationManager` from
|
|
757
|
+
* `@novasamatech/host-api-wrapper`.
|
|
758
|
+
*/
|
|
759
|
+
type NotificationManager = typeof notificationManager;
|
|
760
|
+
/**
|
|
761
|
+
* Host-assigned id for a scheduled notification — pass to
|
|
762
|
+
* {@link NotificationManager.cancel}. Derived from the manager's `push`
|
|
763
|
+
* return type so codec changes surface here as compile errors.
|
|
764
|
+
*/
|
|
765
|
+
type NotificationId = Awaited<ReturnType<NotificationManager["push"]>>;
|
|
766
|
+
/**
|
|
767
|
+
* Push payload: `text`, an optional `deeplink`, and an optional
|
|
768
|
+
* `scheduledAt` (omit for immediate delivery). Derived from the manager's
|
|
769
|
+
* `push` parameter so the shape stays in lockstep with upstream.
|
|
770
|
+
*/
|
|
771
|
+
type PushNotificationInput = Parameters<NotificationManager["push"]>[0];
|
|
772
|
+
/**
|
|
773
|
+
* Get the host notification manager.
|
|
774
|
+
*
|
|
775
|
+
* Returns the shared `notificationManager` singleton from
|
|
776
|
+
* `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable
|
|
777
|
+
* (running outside a host container or the optional peer dep isn't
|
|
778
|
+
* installed).
|
|
779
|
+
*
|
|
780
|
+
* @returns The notification manager, or `null` if unavailable.
|
|
781
|
+
*
|
|
782
|
+
* @example
|
|
783
|
+
* ```ts
|
|
784
|
+
* import { getNotificationManager, PushNotificationError } from "@parity/product-sdk-host";
|
|
785
|
+
*
|
|
786
|
+
* const notifications = await getNotificationManager();
|
|
787
|
+
* if (notifications) {
|
|
788
|
+
* try {
|
|
789
|
+
* const id = await notifications.push({
|
|
790
|
+
* text: "Doors open in 1h",
|
|
791
|
+
* scheduledAt: someUnixMs,
|
|
792
|
+
* });
|
|
793
|
+
* // later: await notifications.cancel(id);
|
|
794
|
+
* } catch (err) {
|
|
795
|
+
* if (err instanceof PushNotificationError.ScheduleLimitReached) {
|
|
796
|
+
* // host hit its pending-notification cap — surface to the user
|
|
797
|
+
* }
|
|
798
|
+
* }
|
|
799
|
+
* }
|
|
800
|
+
* ```
|
|
801
|
+
*/
|
|
802
|
+
declare function getNotificationManager(): Promise<NotificationManager | null>;
|
|
803
|
+
|
|
804
|
+
export { type AccountsProvider, type AllocatableResource, type AllocatableResourceTag, type AllocationOutcome, type AllocationOutcomeTag, BULLETIN_RPCS, ChainNotSupportedError, type ChatBotRegistrationResult, type ChatCustomMessageRenderer, type ChatCustomMessageRendererParams, type ChatManager, type ChatMessageContent, type ChatReceivedAction, type ChatRoom, type ChatRoomRegistrationResult, type ContextualAlias, DEFAULT_BULLETIN_ENDPOINT, type DevicePermissionKind, type HostAccount, type HostLocalStorage, type HostStatementStore, type HostSubscription, type NotificationId, type NotificationManager, type PaymentBalance, type PaymentManager, type PaymentStatus, type PreimageManager, type ProductAccount, type ProductAccountId, type PushNotificationInput, type RemotePermission, type RemotePermissionItem, type RemotePermissionTag, type ResultAsync, type SignedStatement, type Statement, type StatementProof, type StatementTopicFilter, type StatementsPage, type ThemeMode, type ThemeName, type ThemeProvider, type ThemeVariant, type TopUpSource, type Topic, type TruApi, createHostLocalStorage, createHostPreimageManager, createProofAuthorized, deriveEntropy, formatHostError, getAccountsProvider, getChatManager, getHostLocalStorage, getHostProvider, getNotificationManager, getPaymentManager, getPreimageManager, getStatementStore, getThemeProvider, getTruApi, isInsideContainer, isInsideContainerSync, matchChatCustomRenderers, requestDevicePermission, requestPermission, requestResourceAllocation };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createLogger } from '@parity/product-sdk-logger';
|
|
2
2
|
import { enumValue } from '@novasamatech/host-api';
|
|
3
|
-
export { assertEnumVariant, enumValue, fromHex, isEnumVariant, resultErr, resultOk, toHex, unwrapResultOrThrow } from '@novasamatech/host-api';
|
|
3
|
+
export { PushNotificationError, assertEnumVariant, enumValue, fromHex, isEnumVariant, resultErr, resultOk, toHex, unwrapResultOrThrow } from '@novasamatech/host-api';
|
|
4
4
|
|
|
5
5
|
// src/container.ts
|
|
6
6
|
var log = createLogger("host:container");
|
|
@@ -286,7 +286,17 @@ async function getPaymentManager() {
|
|
|
286
286
|
return null;
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
|
+
var log8 = createLogger("host:notifications");
|
|
290
|
+
async function getNotificationManager() {
|
|
291
|
+
try {
|
|
292
|
+
const sdk = await import('@novasamatech/host-api-wrapper');
|
|
293
|
+
return sdk.notificationManager;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
log8.debug("getNotificationManager unavailable", err);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
289
299
|
|
|
290
|
-
export { BULLETIN_RPCS, ChainNotSupportedError, DEFAULT_BULLETIN_ENDPOINT, createHostLocalStorage, createHostPreimageManager, createProofAuthorized, deriveEntropy, formatHostError, getAccountsProvider, getChatManager, getHostLocalStorage, getHostProvider, getPaymentManager, getPreimageManager, getStatementStore, getThemeProvider, getTruApi, isInsideContainer, isInsideContainerSync, matchChatCustomRenderers, requestDevicePermission, requestPermission, requestResourceAllocation };
|
|
300
|
+
export { BULLETIN_RPCS, ChainNotSupportedError, DEFAULT_BULLETIN_ENDPOINT, createHostLocalStorage, createHostPreimageManager, createProofAuthorized, deriveEntropy, formatHostError, getAccountsProvider, getChatManager, getHostLocalStorage, getHostProvider, getNotificationManager, getPaymentManager, getPreimageManager, getStatementStore, getThemeProvider, getTruApi, isInsideContainer, isInsideContainerSync, matchChatCustomRenderers, requestDevicePermission, requestPermission, requestResourceAllocation };
|
|
291
301
|
//# sourceMappingURL=index.js.map
|
|
292
302
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/container.ts","../src/chains.ts","../src/truapi.ts","../src/permissions.ts","../src/theme.ts","../src/entropy.ts","../src/chat.ts","../src/payments.ts"],"names":["log","createLogger","enumValue"],"mappings":"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","file":"index.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport type { JsonRpcProvider } from \"polkadot-api\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { enumValue, type Transport } from \"@novasamatech/host-api\";\n\nimport type { HostLocalStorage, HostStatementStore } from \"./types.js\";\n\nconst log = createLogger(\"host:container\");\n\n/**\n * Thrown by {@link getHostProvider} when the host container is reachable but does\n * not support the requested chain — e.g. the chain isn't enabled in this host\n * build, or the descriptor's genesis hash has drifted from the host's after a\n * network reset.\n *\n * Surfacing this as a thrown error (rather than handing back a provider that\n * silently swallows every JSON-RPC request) is what lets callers of\n * `createChainClient` detect the failure. Without it, the host's fallback no-op\n * provider drops every request on the floor and queries await forever.\n */\nexport class ChainNotSupportedError extends Error {\n /** Genesis hash of the chain the host refused, for programmatic detection. */\n readonly genesisHash: string;\n\n constructor(genesisHash: string) {\n super(\n `Chain ${genesisHash} is not supported by the current host. It may not be enabled in this host build, or its genesis hash may have drifted after a network reset.`,\n );\n this.name = \"ChainNotSupportedError\";\n this.genesisHash = genesisHash;\n }\n}\n\n/**\n * Ask the host whether it can serve the given chain, using the same\n * `host_feature_supported` check the wrapper's provider performs internally\n * before it decides whether to start a real provider or a no-op one.\n *\n * @throws If the host connection never becomes ready, or the host rejects the\n * support check outright. Both are non-hanging, catchable failures.\n */\nasync function isChainSupportedByHost(\n sdk: typeof import(\"@novasamatech/host-api-wrapper\"),\n genesisHash: `0x${string}`,\n): Promise<boolean> {\n const ready = await sdk.sandboxTransport.isReady();\n if (!ready) {\n throw new Error(\n `Host connection did not become ready; cannot verify support for chain ${genesisHash}.`,\n );\n }\n const result = await sdk.hostApi.featureSupported(\n enumValue(\"v1\", enumValue(\"Chain\", genesisHash)),\n );\n return result.match(\n (ok) => ok.value === true,\n (err) => {\n // The reason lives at value.payload.reason for host-protocol errors and\n // value.reason for request-level ones; tolerate both against upstream drift.\n const value = (err as { value?: { payload?: { reason?: string }; reason?: string } })\n ?.value;\n const reason = value?.payload?.reason ?? value?.reason ?? \"unknown reason\";\n throw new Error(`Host rejected the chain-support check for ${genesisHash}: ${reason}`);\n },\n );\n}\n\n/**\n * Detect if running inside a Host container (Polkadot Browser / Polkadot Desktop).\n *\n * The SDK is designed to run exclusively inside a host container. This function\n * is primarily useful for early validation or informational purposes.\n *\n * Uses product-sdk's sandboxProvider as primary detection.\n * Falls back to manual signal checks when product-sdk is not installed.\n */\nexport async function isInsideContainer(): Promise<boolean> {\n if (typeof window === \"undefined\") return false;\n\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.sandboxProvider.isCorrectEnvironment();\n } catch {\n return isInsideContainerSync();\n }\n}\n\n/**\n * Get the Host API localStorage instance when running inside a container.\n * Returns null outside a container or when product-sdk is unavailable.\n */\nexport async function getHostLocalStorage(): Promise<HostLocalStorage | null> {\n if (!(await isInsideContainer())) return null;\n\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.hostLocalStorage as HostLocalStorage;\n } catch (err) {\n log.debug(\"getHostLocalStorage unavailable\", err);\n return null;\n }\n}\n\n/**\n * Construct a fresh host-backed `HostLocalStorage` instance with an optional\n * custom transport. Use this when you need a non-default transport (e.g.\n * for tests); otherwise prefer {@link getHostLocalStorage}, which returns\n * the shared singleton.\n *\n * Mirrors `createLocalStorage` from `@novasamatech/host-api-wrapper`.\n *\n * @param transport - Optional transport; defaults to the sandbox transport.\n * @returns A new `HostLocalStorage` instance, or `null` if unavailable.\n */\nexport async function createHostLocalStorage(\n transport?: Transport,\n): Promise<HostLocalStorage | null> {\n if (!(await isInsideContainer())) return null;\n\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createLocalStorage(transport);\n } catch (err) {\n log.debug(\"createHostLocalStorage unavailable\", err);\n return null;\n }\n}\n\n/**\n * Get a PAPI-compatible JSON-RPC provider that routes through the host connection.\n *\n * When running inside a Polkadot container, this wraps the chain connection via the\n * host's `createPapiProvider`, enabling shared connections and efficient routing.\n * Returns `null` when `@novasamatech/host-api-wrapper` is unavailable or when not\n * running inside a container.\n *\n * @param genesisHash - Genesis hash of the target chain (`0x`-prefixed hex string).\n * @returns A host-routed `JsonRpcProvider`, or `null` if unavailable.\n * @throws {ChainNotSupportedError} When inside a container but the host can't serve\n * the chain — surfaced instead of returning a provider that would hang forever.\n */\nexport async function getHostProvider(genesisHash: `0x${string}`): Promise<JsonRpcProvider | null> {\n let sdk: typeof import(\"@novasamatech/host-api-wrapper\");\n try {\n sdk = await import(\"@novasamatech/host-api-wrapper\");\n } catch (err) {\n // Wrapper not installed — we're not running inside a container.\n log.debug(\"getHostProvider unavailable\", err);\n return null;\n }\n return resolveHostProvider(sdk, genesisHash);\n}\n\n/**\n * Decide whether to build a host provider for `genesisHash`, given the resolved\n * wrapper module. Split out of {@link getHostProvider} so the decision logic can\n * be unit-tested with a fake wrapper, without re-importing the real\n * (browser-only) module.\n *\n * @returns the provider, or `null` when not inside a container.\n * @throws {ChainNotSupportedError} when the host can't serve the chain.\n */\nasync function resolveHostProvider(\n sdk: typeof import(\"@novasamatech/host-api-wrapper\"),\n genesisHash: `0x${string}`,\n): Promise<JsonRpcProvider | null> {\n // Outside a host container there is no provider to hand back. Mirrors\n // createPapiProvider's own environment guard; callers treat null as\n // \"not inside a container\".\n if (!sdk.sandboxTransport.isCorrectEnvironment()) {\n return null;\n }\n\n // Inside a container: confirm the host can actually serve this chain before\n // handing PAPI a provider. When the host doesn't support the chain, the\n // wrapper's fallback provider silently swallows every JSON-RPC request and\n // the caller hangs forever with no rejection. Surface a catchable error.\n if (!(await isChainSupportedByHost(sdk, genesisHash))) {\n throw new ChainNotSupportedError(genesisHash);\n }\n\n return sdk.createPapiProvider(genesisHash);\n}\n\n/**\n * Synchronous container detection — fast heuristic check without product-sdk.\n *\n * Checks for iframe, webview marker, and host message port signals.\n * Use this when you need a quick sync check (e.g., in hot code paths).\n * For full detection including product-sdk, use {@link isInsideContainer} (async).\n */\nexport function isInsideContainerSync(): boolean {\n if (typeof window === \"undefined\") return false;\n\n const win = window as unknown as Record<string, unknown>;\n\n // Iframe detection (polkadot.com browser)\n try {\n if (window !== window.top) return true;\n } catch {\n // Cross-origin iframe — likely inside a container\n return true;\n }\n\n // Webview detection (Polkadot Desktop)\n if (win.__HOST_WEBVIEW_MARK__ === true) return true;\n\n // Desktop message-passing API\n if (win.__HOST_API_PORT__ != null) return true;\n\n return false;\n}\n\n/**\n * Get the host API statement store when running inside a container.\n *\n * Returns a statement store with `subscribe`, `createProof`, and `submit` methods\n * that communicate through the host's native binary protocol — bypassing JSON-RPC\n * entirely. Returns `null` when `@novasamatech/host-api-wrapper` is unavailable.\n *\n * @returns The host statement store, or `null` if unavailable.\n */\nexport async function getStatementStore(): Promise<HostStatementStore | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createStatementStore() as HostStatementStore;\n } catch (err) {\n log.debug(\"getStatementStore unavailable\", err);\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, vi } = import.meta.vitest;\n\n // A self-contained stand-in for the host wrapper, so the chain-support\n // decision can be tested without re-importing the real (browser-only) module.\n const fakeProvider = (() => {}) as unknown as JsonRpcProvider;\n function makeFakeSdk(opts: {\n inContainer?: boolean;\n ready?: boolean;\n supported?: boolean;\n featureErr?: string | null;\n onCreate?: (genesisHash: string) => void;\n }) {\n const { inContainer = true, ready = true, supported = true, featureErr = null } = opts;\n return {\n sandboxTransport: {\n isCorrectEnvironment: () => inContainer,\n isReady: async () => ready,\n },\n hostApi: {\n featureSupported: (_payload: unknown) => ({\n match: (\n okFn: (ok: { tag: string; value: boolean }) => boolean,\n errFn: (err: { value: { payload: { reason: string } } }) => boolean,\n ) =>\n featureErr\n ? errFn({ value: { payload: { reason: featureErr } } })\n : okFn({ tag: \"v1\", value: supported }),\n }),\n },\n createPapiProvider: (genesisHash: string) => {\n opts.onCreate?.(genesisHash);\n return fakeProvider;\n },\n } as unknown as typeof import(\"@novasamatech/host-api-wrapper\");\n }\n\n test(\"returns false in Node environment (no window)\", async () => {\n expect(await isInsideContainer()).toBe(false);\n });\n\n test(\"manualDetection returns true for __HOST_WEBVIEW_MARK__\", async () => {\n const fakeWindow = {\n top: null,\n __HOST_WEBVIEW_MARK__: true,\n };\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true for __HOST_API_PORT__\", async () => {\n const fakeWindow = {\n top: null,\n __HOST_API_PORT__: 12345,\n };\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns false when no signals present\", async () => {\n const fakeWindow = { top: null };\n Object.defineProperty(fakeWindow, \"top\", { get: () => fakeWindow });\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(false);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true for cross-origin iframe\", async () => {\n const fakeWindow = {};\n Object.defineProperty(fakeWindow, \"top\", {\n get: () => {\n throw new DOMException(\"cross-origin\");\n },\n });\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true when window !== window.top (iframe)\", async () => {\n const fakeWindow = { top: {} }; // top is a different object\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"getHostLocalStorage returns null outside container\", async () => {\n expect(await getHostLocalStorage()).toBeNull();\n });\n\n test(\"createHostLocalStorage returns null outside container\", async () => {\n expect(await createHostLocalStorage()).toBeNull();\n });\n\n // --- chain-support gating (resolveHostProvider) ---\n\n test(\"resolves to the provider when supported, and null outside a container\", async () => {\n const created: string[] = [];\n const onCreate = (g: string) => created.push(g);\n\n // Inside a container, supported chain -> real provider.\n expect(await resolveHostProvider(makeFakeSdk({ onCreate }), \"0xabc\")).toBe(fakeProvider);\n // Outside a container -> null, without constructing a provider.\n expect(\n await resolveHostProvider(makeFakeSdk({ inContainer: false, onCreate }), \"0xdef\"),\n ).toBeNull();\n\n expect(created).toEqual([\"0xabc\"]);\n });\n\n test.each([\n { when: \"the host doesn't support the chain\", opts: { supported: false } },\n { when: \"the host connection never becomes ready\", opts: { ready: false } },\n ])(\"throws (and never builds a provider) when $when\", async ({ opts }) => {\n const created: string[] = [];\n const sdk = makeFakeSdk({ ...opts, onCreate: (g) => created.push(g) });\n await expect(resolveHostProvider(sdk, \"0xabc\")).rejects.toThrow();\n // Crucially: no provider is created, so PAPI never receives a hanging no-op.\n expect(created).toEqual([]);\n });\n\n test(\"unsupported chains throw a ChainNotSupportedError carrying the genesis hash\", async () => {\n const err = await resolveHostProvider(makeFakeSdk({ supported: false }), \"0xfeed\").catch(\n (e) => e,\n );\n expect(err).toBeInstanceOf(ChainNotSupportedError);\n expect((err as ChainNotSupportedError).genesisHash).toBe(\"0xfeed\");\n });\n\n test(\"getStatementStore returns null when product-sdk unavailable\", async () => {\n const result = await getStatementStore();\n expect(result).toBeNull();\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Shared chain network configuration — single source of truth for\n * chain-specific endpoints used by multiple packages.\n */\n\n/**\n * Bulletin Chain RPC endpoints per network environment. `paseo` and `summit`\n * are populated today; `polkadot` and `kusama` are reserved for when those\n * Bulletin deployments go live.\n */\nexport const BULLETIN_RPCS = {\n paseo: [\"wss://paseo-bulletin-next-rpc.polkadot.io\"],\n summit: [\"wss://summit-bulletin-rpc.polkadot.io\"],\n polkadot: [] as string[],\n kusama: [] as string[],\n} as const;\n\n/** Default Bulletin Chain endpoint — the first entry under {@link BULLETIN_RPCS}.paseo. */\nexport const DEFAULT_BULLETIN_ENDPOINT: string = BULLETIN_RPCS.paseo[0];\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"chains config\", () => {\n test(\"BULLETIN_RPCS has paseo endpoint\", () => {\n expect(BULLETIN_RPCS.paseo.length).toBeGreaterThan(0);\n expect(BULLETIN_RPCS.paseo[0]).toMatch(/^wss:\\/\\//);\n });\n\n test(\"BULLETIN_RPCS has summit endpoint\", () => {\n expect(BULLETIN_RPCS.summit.length).toBeGreaterThan(0);\n expect(BULLETIN_RPCS.summit[0]).toMatch(/^wss:\\/\\//);\n });\n\n test(\"BULLETIN_RPCS polkadot and kusama are empty until live\", () => {\n expect(BULLETIN_RPCS.polkadot).toEqual([]);\n expect(BULLETIN_RPCS.kusama).toEqual([]);\n });\n\n test(\"DEFAULT_BULLETIN_ENDPOINT matches first paseo endpoint\", () => {\n expect(DEFAULT_BULLETIN_ENDPOINT).toBe(BULLETIN_RPCS.paseo[0]);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * TruAPI - the protocol for communicating between apps and the Polkadot host container.\n *\n * This module centralizes access to @novasamatech/host-api-wrapper and @novasamatech/host-api,\n * allowing other @parity/product-sdk-* packages to import from here rather than depending\n * directly on novasama packages.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { enumValue } from \"@novasamatech/host-api\";\nimport type {\n AllocatableResource as AllocatableResourceCodec,\n AllocationOutcome as AllocationOutcomeCodec,\n CodecType,\n RemotePermission as RemotePermissionCodec,\n} from \"@novasamatech/host-api\";\nimport type { createAccountsProvider, preimageManager } from \"@novasamatech/host-api-wrapper\";\n\nimport { isInsideContainer } from \"./container.js\";\nimport type { Statement, StatementProof } from \"./types.js\";\n\nconst log = createLogger(\"host\");\n\n/**\n * Extract a human-readable message from a host-side error. Hosts wrap\n * errors in versioned envelopes (`{ tag: \"v1\", value: CodecError }`); this\n * helper unwraps the envelope and renders the inner error's `name`/`message`\n * so callers see the host's actual diagnostic instead of `\"[object Object]\"`\n * (from `String(err)`) or a JSON-stringified envelope.\n *\n * Exported for the higher-level wrappers (`requestPermission`,\n * `deriveEntropy`, etc.) that build their `throw new Error(...)` messages.\n */\nexport function formatHostError(err: unknown): string {\n // Single-level unwrap only; nested envelopes fall through to JSON.stringify.\n const inner = isVersionedEnvelope(err) ? err.value : err;\n\n if (inner instanceof Error) return inner.message;\n if (typeof inner === \"string\") return inner;\n if (\n inner != null &&\n typeof inner === \"object\" &&\n \"message\" in inner &&\n typeof (inner as { message: unknown }).message === \"string\"\n ) {\n const named = inner as { name?: unknown; message: string };\n return typeof named.name === \"string\" ? `${named.name}: ${named.message}` : named.message;\n }\n try {\n return JSON.stringify(inner);\n } catch {\n return String(inner);\n }\n}\n\nfunction isVersionedEnvelope(value: unknown): value is { tag: string; value: unknown } {\n return (\n value != null &&\n typeof value === \"object\" &&\n \"tag\" in value &&\n \"value\" in value &&\n typeof (value as { tag: unknown }).tag === \"string\"\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Helpers from @novasamatech/host-api (re-exported from @novasamatech/scale)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport {\n /**\n * Construct an enum variant for TruAPI calls.\n *\n * @example\n * ```ts\n * import { enumValue, getTruApi } from \"@parity/product-sdk-host\";\n *\n * const truApi = await getTruApi();\n * if (truApi) {\n * await truApi.permission([enumValue(\"ChainSubmit\")]);\n * }\n * ```\n */\n enumValue,\n /**\n * Check if a value is a specific enum variant.\n */\n isEnumVariant,\n /**\n * Assert that a value is a specific enum variant, throwing if not.\n */\n assertEnumVariant,\n /**\n * Unwrap a Result, throwing on error.\n */\n unwrapResultOrThrow,\n /**\n * Create an Ok result.\n */\n resultOk,\n /**\n * Create an Err result.\n */\n resultErr,\n /**\n * Convert bytes to hex string.\n */\n toHex,\n /**\n * Convert hex string to bytes.\n */\n fromHex,\n} from \"@novasamatech/host-api\";\n\n/** A `0x`-prefixed hex string (the template literal type ``\\`0x${string}\\``) used by the host API surface for raw byte payloads. Re-exported from `@novasamatech/host-api` so consumers bridging between host APIs and SDK code can reach the host-side type without an additional dependency. */\nexport type { HexString } from \"@novasamatech/host-api\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// TruAPI accessor\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The TruApi type - provides low-level methods for communicating with the host.\n *\n * Methods include:\n * - `navigateTo(url)` - Navigate to a URL within the host\n * - `permission(permissions)` - Request permissions from the host\n * - `localStorageRead/Write/Clear` - Host-backed storage\n * - `sign(payload)` - Request transaction signing\n * - `deriveEntropy(context)` - Derive deterministic entropy\n * - `themeSubscribe()` - Subscribe to host theme changes\n * - And many more...\n *\n * Type identical to `hostApi` from `@novasamatech/host-api-wrapper` so that\n * `truApi.X(...)` calls keep their full inference (return types, method\n * names, parameter shapes) instead of decaying to `any`.\n */\nexport type TruApi = typeof import(\"@novasamatech/host-api-wrapper\").hostApi;\n\n/** Cached TruApi instance */\nlet cachedTruApi: TruApi | null = null;\n\n/**\n * Get the TruAPI instance for direct low-level access.\n *\n * Returns the `hostApi` object from `@novasamatech/host-api-wrapper` which provides\n * methods for communicating directly with the host container. Returns `null`\n * when running outside a container or when the SDK is unavailable.\n *\n * For most use cases, prefer the higher-level functions like `getHostLocalStorage()`,\n * `getHostProvider()`, etc. Use this when you need direct access to host methods\n * like `navigateTo()`, `permission()`, or `deriveEntropy()`.\n *\n * @example\n * ```ts\n * import { getTruApi, enumValue } from \"@parity/product-sdk-host\";\n *\n * const truApi = await getTruApi();\n * if (truApi) {\n * // Request permission\n * const result = await truApi.permission([enumValue(\"ChainSubmit\")]);\n *\n * // Navigate to a URL\n * await truApi.navigateTo(\"polkadot://settings\");\n *\n * // Subscribe to theme changes\n * const sub = truApi.themeSubscribe(undefined, (theme) => {\n * console.log(\"Theme changed:\", theme);\n * });\n * }\n * ```\n *\n * @returns The TruAPI instance, or `null` if unavailable.\n */\nexport async function getTruApi(): Promise<TruApi | null> {\n if (cachedTruApi) return cachedTruApi;\n\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n cachedTruApi = sdk.hostApi;\n log.debug(\"TruAPI loaded\");\n return cachedTruApi;\n } catch {\n log.debug(\"TruAPI unavailable (not in container or SDK not installed)\");\n return null;\n }\n}\n\n/**\n * Get the preimage manager for bulletin chain operations.\n *\n * The preimage manager handles uploading and looking up preimages (arbitrary data)\n * on the bulletin chain through the host's optimized path.\n *\n * @returns The preimage manager, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getPreimageManager } from \"@parity/product-sdk-host\";\n *\n * const manager = await getPreimageManager();\n * if (manager) {\n * // Submit a preimage\n * const key = await manager.submit(new Uint8Array([1, 2, 3]));\n *\n * // Look up a preimage\n * const sub = manager.lookup(key, (data) => {\n * if (data) console.log(\"Found:\", data);\n * });\n * }\n * ```\n */\nexport async function getPreimageManager(): Promise<PreimageManager | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.preimageManager;\n } catch (err) {\n log.debug(\"getPreimageManager unavailable\", err);\n return null;\n }\n}\n\n/**\n * Preimage manager handle for bulletin chain operations. `lookup` returns a\n * `Subscription<void>` (`unsubscribe` + `onInterrupt`); `submit` returns a\n * `0x`-prefixed hex preimage key.\n *\n * Type identical to `preimageManager` from `@novasamatech/host-api-wrapper`.\n */\nexport type PreimageManager = typeof preimageManager;\n\n/**\n * Construct a fresh `PreimageManager` instance with an optional custom\n * transport. Use this when you need a non-default transport; otherwise\n * prefer {@link getPreimageManager}, which returns the shared singleton.\n *\n * Mirrors `createPreimageManager` from `@novasamatech/host-api-wrapper`.\n *\n * @param transport - Optional transport; defaults to the sandbox transport.\n * @returns A new `PreimageManager` instance, or `null` if unavailable.\n */\nexport async function createHostPreimageManager(\n transport?: import(\"@novasamatech/host-api\").Transport,\n): Promise<PreimageManager | null> {\n if (!(await isInsideContainer())) return null;\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createPreimageManager(transport);\n } catch (err) {\n log.debug(\"createHostPreimageManager unavailable\", err);\n return null;\n }\n}\n\n/**\n * Get the accounts provider for managing host accounts.\n *\n * @returns The accounts provider, or `null` if unavailable.\n */\nexport async function getAccountsProvider(): Promise<AccountsProvider | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createAccountsProvider();\n } catch (err) {\n log.debug(\"getAccountsProvider unavailable\", err);\n return null;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Resource allocation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Resource types requestable via {@link requestResourceAllocation}.\n * Derived from the upstream codec so variant renames surface as compile\n * errors, not runtime failures.\n */\nexport type AllocatableResource = CodecType<typeof AllocatableResourceCodec>;\n\n/** Tag-only view of {@link AllocatableResource} for places that just need the variant name. */\nexport type AllocatableResourceTag = AllocatableResource[\"tag\"];\n\n/**\n * Per-resource outcome from {@link requestResourceAllocation}.\n * The host strips secret payloads from `Allocated` before returning, so\n * `value` is always `undefined` on the product side.\n */\nexport type AllocationOutcome = CodecType<typeof AllocationOutcomeCodec>;\n\n/** Tag-only view of {@link AllocationOutcome} (`\"Allocated\" | \"Rejected\" | \"NotAvailable\"`). */\nexport type AllocationOutcomeTag = AllocationOutcome[\"tag\"];\n\n/**\n * Remote permission the dapp can ask the host to grant via\n * {@link requestPermission}.\n *\n * Derived from the upstream codec so variant renames surface as compile\n * errors, not runtime failures.\n */\nexport type RemotePermission = CodecType<typeof RemotePermissionCodec>;\n\n/** Tag-only view of {@link RemotePermission}. */\nexport type RemotePermissionTag = RemotePermission[\"tag\"];\n\n/**\n * Request the host to pre-allocate one or more resource allowances.\n *\n * The host prompts the user once; subsequent operations covered by the\n * granted allowance don't re-prompt.\n *\n * @param resources - Resources to request.\n * @returns Per-resource outcomes in the same order as `resources`.\n * @throws If the host is unavailable or the request fails.\n *\n * @example\n * ```ts\n * const outcomes = await requestResourceAllocation([\n * { tag: \"BulletinAllowance\", value: undefined },\n * ]);\n * if (outcomes[0].tag === \"Allocated\") { ... }\n * ```\n */\nexport async function requestResourceAllocation(\n resources: AllocatableResource[],\n): Promise<AllocationOutcome[]> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"requestResourceAllocation: TruAPI unavailable\");\n }\n log.debug(\"requestResourceAllocation\", { resources: resources.map((r) => r.tag) });\n\n // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.\n return await truApi.requestResourceAllocation(enumValue(\"v1\", resources)).match(\n (envelope: { tag: \"v1\"; value: AllocationOutcome[] }) => envelope.value,\n (err: unknown) => {\n throw new Error(`requestResourceAllocation failed: ${formatHostError(err)}`, {\n cause: err,\n });\n },\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Authorized Statement Store proof creation (RFC-10 §\"Statement Store allowance\")\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Have the host sign a Statement using an allowance-bearing account it\n * picks internally — RFC-10 §\"Statement Store allowance\".\n *\n * The product passes only the Statement payload; the host chooses the\n * `//allowance//statement-store//{productId}` account that holds SSS\n * allowance and signs with it. Allowance is provisioned implicitly on\n * first use if the host hasn't already pre-allocated via\n * {@link requestResourceAllocation}; products never see the signing\n * account or its key material.\n *\n * Pairs with {@link getStatementStore}'s `submit`: call this to obtain\n * a proof, attach it to the Statement, and submit the result.\n *\n * @param statement - The Statement to be signed.\n * @returns The proof to attach before submitting.\n * @throws If the host is unavailable or the host-side signing fails.\n *\n * @example\n * ```ts\n * import { createProofAuthorized, getStatementStore } from \"@parity/product-sdk-host\";\n *\n * const statement = {\n * proof: undefined,\n * decryptionKey: undefined,\n * expiry: undefined,\n * channel: undefined,\n * topics: [],\n * data: payload,\n * };\n * const proof = await createProofAuthorized(statement);\n * const store = await getStatementStore();\n * await store?.submit({ ...statement, proof });\n * ```\n *\n * @remarks\n * RFC-10 introduces this as a new, strictly additive TruAPI call. The\n * pre-existing `HostStatementStore.createProof(accountId, statement)`\n * surface stays available for products that own a non-allowance signing\n * account; this wrapper is the sponsored-submission path.\n */\nexport async function createProofAuthorized(statement: Statement): Promise<StatementProof> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"createProofAuthorized: TruAPI unavailable\");\n }\n log.debug(\"createProofAuthorized\", {\n topics: statement.topics.length,\n dataLen: statement.data?.length ?? 0,\n });\n\n // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.\n return await truApi.statementStoreCreateProofAuthorized(enumValue(\"v1\", statement)).match(\n (envelope: { tag: \"v1\"; value: StatementProof }) => envelope.value,\n (err: unknown) => {\n throw new Error(`createProofAuthorized failed: ${formatHostError(err)}`, {\n cause: err,\n });\n },\n );\n}\n\n/**\n * One of the user's existing wallet accounts, surfaced through the host and\n * identified by its public key and an optional name. Contrast with\n * {@link ProductAccount}, which is also user-controlled but derived by the\n * host for a specific app rather than picked from the user's existing keys.\n */\nexport interface HostAccount {\n publicKey: Uint8Array;\n name?: string;\n}\n\n/**\n * A product account — an app-scoped derived account managed by the host wallet.\n *\n * The host derives a unique keypair for each app (identified by `dotNsIdentifier`)\n * so apps get their own account that the user controls but is scoped to the app.\n */\nexport interface ProductAccount {\n /** App identifier (e.g., \"mark3t.dot\"). */\n dotNsIdentifier: string;\n /** Derivation index within the app scope. Default: 0 */\n derivationIndex: number;\n /** Raw public key (32 bytes). */\n publicKey: Uint8Array;\n}\n\n/**\n * A contextual alias obtained from Ring VRF.\n *\n * Proves account membership in a ring without revealing which account.\n */\nexport interface ContextualAlias {\n /** Ring context (32 bytes). */\n context: Uint8Array;\n /** The Ring VRF alias bytes. */\n alias: Uint8Array;\n}\n\n/**\n * Neverthrow-style ResultAsync returned by product-sdk methods.\n *\n * Use `.match(onOk, onErr)` to handle success/error cases.\n */\nexport interface ResultAsync<T, E> {\n match: <A, B = A>(ok: (t: T) => A, err: (e: E) => B) => Promise<A | B>;\n}\n\n/**\n * Accounts provider handle from `@novasamatech/host-api-wrapper`. Surfaces the\n * full upstream API - host wallet accounts, app-scoped product accounts,\n * Ring VRF, user identity (`getUserId`, `requestLogin`), and connection\n * status subscription.\n *\n * Type identical to `createAccountsProvider()` from\n * `@novasamatech/host-api-wrapper`; methods return neverthrow `ResultAsync`\n * values with typed `CodecError` variants in the error channel.\n */\nexport type AccountsProvider = ReturnType<typeof createAccountsProvider>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Tests\n// ─────────────────────────────────────────────────────────────────────────────\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getTruApi returns TruApi when SDK is available\", async () => {\n // Reset cache for test\n cachedTruApi = null;\n const api = await getTruApi();\n // In dev/test mode, product-sdk is installed\n expect(api === null || typeof api === \"object\").toBe(true);\n });\n\n test(\"getPreimageManager returns manager when SDK is available\", async () => {\n const manager = await getPreimageManager();\n // In dev/test mode, product-sdk is installed\n expect(manager === null || typeof manager === \"object\").toBe(true);\n });\n\n test(\"createHostPreimageManager returns null outside container\", async () => {\n expect(await createHostPreimageManager()).toBeNull();\n });\n\n test(\"formatHostError unwraps versioned envelopes and renders CodecError\", () => {\n expect(\n formatHostError({\n tag: \"v1\",\n value: { name: \"GenericError\", message: \"boom\" },\n }),\n ).toBe(\"GenericError: boom\");\n expect(formatHostError(new Error(\"plain\"))).toBe(\"plain\");\n expect(formatHostError(\"string err\")).toBe(\"string err\");\n expect(formatHostError({ tag: \"v1\", value: { message: \"no-name\" } })).toBe(\"no-name\");\n });\n\n test(\"getAccountsProvider returns provider when SDK is available\", async () => {\n // In dev/test mode, product-sdk is installed, so this returns a provider\n const provider = await getAccountsProvider();\n // Just verify it returns something (null when SDK unavailable, provider when available)\n expect(provider === null || typeof provider === \"object\").toBe(true);\n });\n\n test(\"enumValue is exported\", async () => {\n const { enumValue } = await import(\"./truapi.js\");\n expect(typeof enumValue).toBe(\"function\");\n });\n\n test(\"requestResourceAllocation throws when TruAPI is unavailable\", async () => {\n cachedTruApi = null;\n const api = await getTruApi();\n if (api === null) {\n await expect(\n requestResourceAllocation([{ tag: \"BulletinAllowance\", value: undefined }]),\n ).rejects.toThrow(/TruAPI unavailable/);\n } else {\n expect(typeof requestResourceAllocation).toBe(\"function\");\n }\n });\n\n test(\"createProofAuthorized throws when TruAPI is unavailable\", async () => {\n cachedTruApi = null;\n const api = await getTruApi();\n if (api === null) {\n await expect(\n createProofAuthorized({\n proof: undefined,\n decryptionKey: undefined,\n expiry: undefined,\n channel: undefined,\n topics: [],\n data: undefined,\n }),\n ).rejects.toThrow(/TruAPI unavailable/);\n } else {\n expect(typeof createProofAuthorized).toBe(\"function\");\n }\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Higher-level wrappers for the host's single-permission flows.\n *\n * `hostApi.permission` / `hostApi.devicePermission` take a versioned\n * envelope (`enumValue(\"v1\", ...)`) and return a neverthrow `ResultAsync`\n * of an unwrapped versioned response. Consumers rebuild that wrap/unwrap\n * dance every time. {@link requestPermission} and\n * {@link requestDevicePermission} collapse it to one-liners that match the\n * shape of {@link requestResourceAllocation} (throws on error, returns\n * the unwrapped payload on success).\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type { CodecType } from \"@novasamatech/host-api\";\nimport type { DevicePermission as DevicePermissionCodec } from \"@novasamatech/host-api\";\n\nimport { enumValue, formatHostError, getTruApi, type RemotePermission } from \"./truapi.js\";\n\nconst log = createLogger(\"host:permissions\");\n\n/**\n * Device permission the dapp can ask the host to grant via\n * {@link requestDevicePermission}.\n *\n * Derived from the upstream codec so variant renames surface as compile\n * errors, not runtime failures.\n */\nexport type DevicePermissionKind = CodecType<typeof DevicePermissionCodec>;\n\n/**\n * Alias of {@link RemotePermission} matching the upstream\n * `@novasamatech/host-api-wrapper` name. Use either freely.\n */\nexport type RemotePermissionItem = RemotePermission;\n\n/**\n * Request a single remote permission from the host.\n *\n * Builds the `v1` envelope, calls `hostApi.permission`, unwraps the response,\n * and returns the host's boolean granted/denied outcome.\n *\n * @param permission - The remote permission to request.\n * @returns `true` if the host granted the permission, `false` if denied.\n * @throws If the host is unavailable or the request fails.\n *\n * @example\n * ```ts\n * const granted = await requestPermission({ tag: \"ChainSubmit\", value: undefined });\n * if (!granted) {\n * tellUserToReconnect();\n * }\n * ```\n */\nexport async function requestPermission(permission: RemotePermission): Promise<boolean> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"requestPermission: TruAPI unavailable\");\n }\n log.debug(\"requestPermission\", { tag: permission.tag });\n\n return await truApi.permission(enumValue(\"v1\", permission)).match(\n (envelope: { tag: \"v1\"; value: boolean }) => envelope.value,\n (err: unknown) => {\n throw new Error(`requestPermission failed: ${formatHostError(err)}`, { cause: err });\n },\n );\n}\n\n/**\n * Request a single device permission (camera, microphone, etc.) from the\n * host.\n *\n * Builds the `v1` envelope, calls `hostApi.devicePermission`, unwraps the\n * response, and returns the host's boolean granted/denied outcome.\n *\n * @param permission - The device permission to request.\n * @returns `true` if the host granted the permission, `false` if denied.\n * @throws If the host is unavailable or the request fails.\n *\n * @example\n * ```ts\n * const granted = await requestDevicePermission(\"Camera\");\n * if (!granted) {\n * showCameraDeniedMessage();\n * }\n * ```\n */\nexport async function requestDevicePermission(permission: DevicePermissionKind): Promise<boolean> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"requestDevicePermission: TruAPI unavailable\");\n }\n log.debug(\"requestDevicePermission\", { permission });\n\n return await truApi.devicePermission(enumValue(\"v1\", permission)).match(\n (envelope: { tag: \"v1\"; value: boolean }) => envelope.value,\n (err: unknown) => {\n throw new Error(`requestDevicePermission failed: ${formatHostError(err)}`, {\n cause: err,\n });\n },\n );\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe, vi } = import.meta.vitest;\n\n async function withMockedTruApi<T>(\n bridge: {\n permission?: (req: unknown) => unknown;\n devicePermission?: (req: unknown) => unknown;\n } | null,\n fn: (mod: typeof import(\"./permissions.js\")) => Promise<T>,\n ): Promise<T> {\n vi.resetModules();\n vi.doMock(\"./truapi.js\", async (importOriginal) => {\n const original = await importOriginal<typeof import(\"./truapi.js\")>();\n return {\n ...original,\n getTruApi: async () => bridge,\n enumValue: (version: string, value: unknown) => ({ tag: version, value }),\n };\n });\n try {\n const mod = await import(\"./permissions.js\");\n return await fn(mod);\n } finally {\n vi.doUnmock(\"./truapi.js\");\n vi.resetModules();\n }\n }\n\n describe(\"requestPermission\", () => {\n test(\"throws when TruAPI is unavailable\", async () => {\n await withMockedTruApi(null, async (mod) => {\n await expect(\n mod.requestPermission({ tag: \"ChainSubmit\", value: undefined }),\n ).rejects.toThrow(/TruAPI unavailable/);\n });\n });\n\n test(\"unwraps the v1 boolean outcome\", async () => {\n await withMockedTruApi(\n {\n permission: vi.fn().mockReturnValue({\n match: async (onOk: (v: unknown) => unknown) =>\n onOk({ tag: \"v1\", value: true }),\n }),\n },\n async (mod) => {\n const granted = await mod.requestPermission({\n tag: \"ChainSubmit\",\n value: undefined,\n });\n expect(granted).toBe(true);\n },\n );\n });\n\n test(\"wraps host errors with a diagnostic message\", async () => {\n await withMockedTruApi(\n {\n permission: vi.fn().mockReturnValue({\n match: async (\n _onOk: (v: unknown) => unknown,\n onErr: (e: unknown) => unknown,\n ) =>\n onErr({\n tag: \"v1\",\n value: { name: \"GenericError\", message: \"boom\" },\n }),\n }),\n },\n async (mod) => {\n await expect(\n mod.requestPermission({ tag: \"ChainSubmit\", value: undefined }),\n ).rejects.toThrow(/requestPermission failed: GenericError: boom/);\n },\n );\n });\n });\n\n describe(\"requestDevicePermission\", () => {\n test(\"throws when TruAPI is unavailable\", async () => {\n await withMockedTruApi(null, async (mod) => {\n await expect(mod.requestDevicePermission(\"Camera\")).rejects.toThrow(\n /TruAPI unavailable/,\n );\n });\n });\n\n test(\"unwraps the v1 boolean outcome\", async () => {\n await withMockedTruApi(\n {\n devicePermission: vi.fn().mockReturnValue({\n match: async (onOk: (v: unknown) => unknown) =>\n onOk({ tag: \"v1\", value: true }),\n }),\n },\n async (mod) => {\n const granted = await mod.requestDevicePermission(\"Camera\");\n expect(granted).toBe(true);\n },\n );\n });\n\n test(\"wraps host errors with a diagnostic message\", async () => {\n await withMockedTruApi(\n {\n devicePermission: vi.fn().mockReturnValue({\n match: async (\n _onOk: (v: unknown) => unknown,\n onErr: (e: unknown) => unknown,\n ) =>\n onErr({\n tag: \"v1\",\n value: { name: \"GenericError\", message: \"boom\" },\n }),\n }),\n },\n async (mod) => {\n await expect(mod.requestDevicePermission(\"Camera\")).rejects.toThrow(\n /requestDevicePermission failed: GenericError: boom/,\n );\n },\n );\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Higher-level wrapper for the host's theme subscription.\n *\n * `hostApi.themeSubscribe` is reachable via {@link getTruApi}, but consumers\n * have to wire the subscription envelope themselves. `getThemeProvider`\n * returns the `@novasamatech/host-api-wrapper` theme provider object directly,\n * giving callers a `subscribeTheme(cb)` method that resolves to a typed\n * {@link ThemeMode} — a `{ name, variant }` struct where `variant` is\n * `\"Light\" | \"Dark\"` — and yields a `Subscription<void>` handle.\n *\n * @remarks\n * As of `host-api(-wrapper)` v0.8 the theme payload is a struct, not a flat\n * `\"light\" | \"dark\"` string: read {@link ThemeMode.variant} for the\n * light/dark value (now capitalized) and {@link ThemeMode.name} for the\n * active theme name (`Default`, or `Custom` carrying a string id).\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type {\n createThemeProvider,\n ThemeMode as NovasamaThemeMode,\n} from \"@novasamatech/host-api-wrapper\";\n\nconst log = createLogger(\"host:theme\");\n\n/**\n * Host theme provider handle. Exposes `subscribeTheme(callback)` which\n * receives a typed {@link ThemeMode} struct on every change and returns a\n * `Subscription<void>` (`unsubscribe` + `onInterrupt`).\n *\n * Type identical to `createThemeProvider()` from\n * `@novasamatech/host-api-wrapper`.\n */\nexport type ThemeProvider = ReturnType<typeof createThemeProvider>;\n\n/**\n * Host theme value. Re-exported from `@novasamatech/host-api-wrapper`.\n *\n * A `{ name, variant }` struct as of v0.8 (previously a flat\n * `\"light\" | \"dark\"` string).\n */\nexport type ThemeMode = NovasamaThemeMode;\n\n/** Light/dark variant of the active theme: `\"Light\" | \"Dark\"`. */\nexport type ThemeVariant = ThemeMode[\"variant\"];\n\n/**\n * Active theme name: `{ tag: \"Default\" }`, or `{ tag: \"Custom\", value }`\n * carrying the custom theme's string id.\n */\nexport type ThemeName = ThemeMode[\"name\"];\n\n/**\n * Get the host theme provider.\n *\n * Returns the theme-subscription handle exported by\n * `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable\n * (running outside a host container or the optional peer dep isn't\n * installed).\n *\n * Implementation note: upstream `@novasamatech/host-api-wrapper` exports only\n * the `createThemeProvider` factory and no `themeProvider` singleton, so\n * this getter constructs a fresh instance on each call (unlike\n * {@link getPreimageManager} or {@link getHostLocalStorage}, which return\n * upstream singletons). The constructed provider is cheap to allocate; it\n * only opens a subscription when `subscribeTheme` is called.\n *\n * @returns The theme provider, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getThemeProvider } from \"@parity/product-sdk-host\";\n *\n * const provider = await getThemeProvider();\n * if (provider) {\n * const sub = provider.subscribeTheme((theme) => {\n * document.documentElement.dataset.theme = theme.variant.toLowerCase();\n * if (theme.name.tag === \"Custom\") loadCustomTheme(theme.name.value);\n * });\n * // sub.unsubscribe() to stop listening\n * }\n * ```\n */\nexport async function getThemeProvider(): Promise<ThemeProvider | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createThemeProvider();\n } catch (err) {\n log.debug(\"getThemeProvider unavailable\", err);\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getThemeProvider returns provider when SDK is available\", async () => {\n const provider = await getThemeProvider();\n expect(provider === null || typeof provider === \"object\").toBe(true);\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Higher-level wrapper for the host's entropy derivation (RFC-0007).\n *\n * `hostApi.deriveEntropy` is reachable via {@link getTruApi}, but consumers\n * have to wrap the value in the versioned envelope (`enumValue(\"v1\", ...)`)\n * and unwrap the neverthrow `ResultAsync` themselves. `deriveEntropy`\n * collapses that to a throw-on-error Promise that matches the shape of\n * {@link requestPermission} and {@link requestResourceAllocation}.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { enumValue, formatHostError, getTruApi } from \"./truapi.js\";\n\nconst log = createLogger(\"host:entropy\");\n\n/**\n * Derive deterministic entropy from a context key (RFC-0007).\n *\n * The host derives entropy from the user's wallet + the provided context\n * key. Calling with the same key on the same wallet yields the same bytes;\n * different keys (or different wallets) yield uncorrelated entropy.\n *\n * @param key - Context key bytes (typically a SCALE-encoded discriminator).\n * @returns The derived entropy bytes.\n * @throws If the host is unavailable or the host-side derivation fails.\n *\n * @example\n * ```ts\n * import { deriveEntropy } from \"@parity/product-sdk-host\";\n *\n * const seed = await deriveEntropy(new TextEncoder().encode(\"my-app:seed-v1\"));\n * ```\n */\nexport async function deriveEntropy(key: Uint8Array): Promise<Uint8Array> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"deriveEntropy: TruAPI unavailable\");\n }\n log.debug(\"deriveEntropy\", { keyLen: key.length });\n\n return await truApi.deriveEntropy(enumValue(\"v1\", key)).match(\n (envelope: { tag: \"v1\"; value: Uint8Array }) => envelope.value,\n (err: unknown) => {\n throw new Error(`deriveEntropy failed: ${formatHostError(err)}`, { cause: err });\n },\n );\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"deriveEntropy throws when TruAPI is unavailable\", async () => {\n const api = await getTruApi();\n if (api === null) {\n await expect(deriveEntropy(new Uint8Array([1, 2, 3]))).rejects.toThrow(\n /TruAPI unavailable/,\n );\n } else {\n expect(typeof deriveEntropy).toBe(\"function\");\n }\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Wrapper for the host's chat surface (`host_chat_*` family).\n *\n * Shipped flat-in-host rather than as `getTruApi().chat.*` (the shape\n * sketched in issue #93) because the upstream JS `hostApi` is itself a\n * flat object - there is no `.chat` accessor to mirror. A flat\n * `getChatManager()` matches the pattern already used by\n * {@link getThemeProvider}, {@link getAccountsProvider}, and\n * {@link getStatementStore}; if a namespaced view is desirable later, it\n * can be layered on top without breaking this surface.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type {\n ChatBotRegistrationResult as NovasamaChatBotRegistrationResult,\n ChatCustomMessageRenderer as NovasamaChatCustomMessageRenderer,\n ChatCustomMessageRendererParams as NovasamaChatCustomMessageRendererParams,\n ChatMessageContent as NovasamaChatMessageContent,\n ChatReceivedAction as NovasamaChatReceivedAction,\n ChatRoom as NovasamaChatRoom,\n ChatRoomRegistrationResult as NovasamaChatRoomRegistrationResult,\n createProductChatManager,\n} from \"@novasamatech/host-api-wrapper\";\n\nconst log = createLogger(\"host:chat\");\n\n/** Chat message payload variants. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatMessageContent = NovasamaChatMessageContent;\n\n/** Action received via {@link ChatManager.subscribeAction}. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatReceivedAction = NovasamaChatReceivedAction;\n\n/** Room metadata delivered to {@link ChatManager.subscribeChatList}. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatRoom = NovasamaChatRoom;\n\n/** Result of registering a chat room (`\"New\" | \"Exists\"`). Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatRoomRegistrationResult = NovasamaChatRoomRegistrationResult;\n\n/** Result of registering a bot (`\"New\" | \"Exists\"`). Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatBotRegistrationResult = NovasamaChatBotRegistrationResult;\n\n/** Renderer callback for custom message types. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatCustomMessageRenderer = NovasamaChatCustomMessageRenderer;\n\n/** Parameters passed to a {@link ChatCustomMessageRenderer}. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatCustomMessageRendererParams<T = Uint8Array> =\n NovasamaChatCustomMessageRendererParams<T>;\n\n/**\n * Chat manager handle. Exposes room/bot registration, message sending,\n * subscription to room list and incoming actions, and custom-renderer\n * registration.\n *\n * Type identical to `createProductChatManager()` from\n * `@novasamatech/host-api-wrapper`.\n */\nexport type ChatManager = ReturnType<typeof createProductChatManager>;\n\n/**\n * Get the host chat manager.\n *\n * Returns the chat manager from `@novasamatech/host-api-wrapper`, or `null` if\n * the package is unavailable (running outside a host container or the\n * optional peer dep isn't installed).\n *\n * @returns The chat manager, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getChatManager } from \"@parity/product-sdk-host\";\n *\n * const chat = await getChatManager();\n * if (chat) {\n * await chat.registerBot({ botId: \"echo\", name: \"Echo Bot\", icon: \"\" });\n * chat.subscribeAction((action) => { ... });\n * }\n * ```\n */\nexport async function getChatManager(): Promise<ChatManager | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createProductChatManager();\n } catch (err) {\n log.debug(\"getChatManager unavailable\", err);\n return null;\n }\n}\n\n/**\n * Dispatch helper that composes multiple custom-message renderers into a\n * single {@link ChatCustomMessageRenderer} keyed by `messageType`.\n *\n * Mirrors `matchChatCustomRenderers` from `@novasamatech/host-api-wrapper`\n * inline (the upstream implementation is pure dispatch logic with no\n * transport / runtime dependency on Novasama), so callers get the same\n * sync signature instead of an async-with-null wrapper.\n *\n * @param map - Object mapping `messageType` strings to renderers.\n * @returns A composed renderer that dispatches to the entry matching\n * `params.messageType`, or throws if no renderer is registered.\n */\nexport function matchChatCustomRenderers(\n map: Record<string, ChatCustomMessageRenderer>,\n): ChatCustomMessageRenderer {\n return (params, render) => {\n const renderer = map[params.messageType];\n if (!renderer) {\n throw new Error(`Renderer for message type ${params.messageType} is not defined`);\n }\n return renderer(params, render);\n };\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getChatManager returns manager when SDK is available\", async () => {\n const chat = await getChatManager();\n expect(chat === null || typeof chat === \"object\").toBe(true);\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Wrapper for the host's payment manager (RFC-0006).\n *\n * Shipped flat-in-host rather than as `getTruApi().payment.*` because the\n * upstream JS `hostApi` is itself a flat object - there is no `.payment`\n * accessor to mirror. A flat `getPaymentManager()` matches the singleton\n * pattern already used by {@link getPreimageManager},\n * {@link getHostLocalStorage}, and {@link getAccountsProvider}.\n *\n * Returns the shared `paymentManager` singleton from\n * `@novasamatech/host-api-wrapper` (not a fresh `createPaymentManager()`\n * instance) so callers share one wrapper + hostApi closure across the app.\n *\n * Distinct from the CoinPayment / merchant-payments surface tracked under\n * `@parity/product-sdk-merchant-payments` (RFC-0017). RFC-0006 is the\n * user-initiated balance / top-up / payment-request flow; RFC-0017 is the\n * merchant-initiated checkout flow.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type {\n PaymentBalance as NovasamaPaymentBalance,\n PaymentStatus as NovasamaPaymentStatus,\n TopUpSource as NovasamaTopUpSource,\n paymentManager,\n} from \"@novasamatech/host-api-wrapper\";\n\nconst log = createLogger(\"host:payments\");\n\n/** Available balance for the user's payment account. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type PaymentBalance = NovasamaPaymentBalance;\n\n/** Status of an in-flight payment request. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type PaymentStatus = NovasamaPaymentStatus;\n\n/** Source for {@link PaymentManager.topUp}. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type TopUpSource = NovasamaTopUpSource;\n\n/**\n * Payment manager handle. Exposes balance subscription, top-up, payment\n * requests, and payment status subscription.\n *\n * Type identical to `paymentManager` from `@novasamatech/host-api-wrapper`.\n */\nexport type PaymentManager = typeof paymentManager;\n\n/**\n * Get the host payment manager.\n *\n * Returns the shared `paymentManager` singleton from\n * `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable\n * (running outside a host container or the optional peer dep isn't\n * installed).\n *\n * @returns The payment manager, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getPaymentManager } from \"@parity/product-sdk-host\";\n *\n * const payments = await getPaymentManager();\n * if (payments) {\n * const sub = payments.subscribeBalance((b) => { ... });\n * await payments.topUp(1_000_000n, { type: \"productAccount\", derivationIndex: 0 });\n * const destination = new Uint8Array(32);\n * const { id } = await payments.requestPayment(500n, destination);\n * sub.unsubscribe();\n * }\n * ```\n */\nexport async function getPaymentManager(): Promise<PaymentManager | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.paymentManager;\n } catch (err) {\n log.debug(\"getPaymentManager unavailable\", err);\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getPaymentManager returns manager with full RFC-0006 surface when SDK is available\", async () => {\n const payments = await getPaymentManager();\n if (payments === null) {\n // Acceptable: SDK couldn't load (e.g. peer dep missing in some envs).\n return;\n }\n expect(typeof payments.subscribeBalance).toBe(\"function\");\n expect(typeof payments.topUp).toBe(\"function\");\n expect(typeof payments.requestPayment).toBe(\"function\");\n expect(typeof payments.subscribePaymentStatus).toBe(\"function\");\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/container.ts","../src/chains.ts","../src/truapi.ts","../src/permissions.ts","../src/theme.ts","../src/entropy.ts","../src/chat.ts","../src/payments.ts","../src/notifications.ts"],"names":["log","createLogger","enumValue"],"mappings":";;;;;AAQA,IAAM,GAAA,GAAM,aAAa,gBAAgB,CAAA;AAalC,IAAM,sBAAA,GAAN,cAAqC,KAAA,CAAM;AAAA;AAAA,EAErC,WAAA;AAAA,EAET,YAAY,WAAA,EAAqB;AAC7B,IAAA,KAAA;AAAA,MACI,SAAS,WAAW,CAAA,4IAAA;AAAA,KACxB;AACA,IAAA,IAAA,CAAK,IAAA,GAAO,wBAAA;AACZ,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AAAA,EACvB;AACJ;AAUA,eAAe,sBAAA,CACX,KACA,WAAA,EACgB;AAChB,EAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,CAAI,gBAAA,CAAiB,OAAA,EAAQ;AACjD,EAAA,IAAI,CAAC,KAAA,EAAO;AACR,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,yEAAyE,WAAW,CAAA,CAAA;AAAA,KACxF;AAAA,EACJ;AACA,EAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,OAAA,CAAQ,gBAAA;AAAA,IAC7B,SAAA,CAAU,IAAA,EAAM,SAAA,CAAU,OAAA,EAAS,WAAW,CAAC;AAAA,GACnD;AACA,EAAA,OAAO,MAAA,CAAO,KAAA;AAAA,IACV,CAAC,EAAA,KAAO,EAAA,CAAG,KAAA,KAAU,IAAA;AAAA,IACrB,CAAC,GAAA,KAAQ;AAGL,MAAA,MAAM,QAAS,GAAA,EACT,KAAA;AACN,MAAA,MAAM,MAAA,GAAS,KAAA,EAAO,OAAA,EAAS,MAAA,IAAU,OAAO,MAAA,IAAU,gBAAA;AAC1D,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0CAAA,EAA6C,WAAW,CAAA,EAAA,EAAK,MAAM,CAAA,CAAE,CAAA;AAAA,IACzF;AAAA,GACJ;AACJ;AAWA,eAAsB,iBAAA,GAAsC;AACxD,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,KAAA;AAE1C,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,GAAA,CAAI,gBAAgB,oBAAA,EAAqB;AAAA,EACpD,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,qBAAA,EAAsB;AAAA,EACjC;AACJ;AAMA,eAAsB,mBAAA,GAAwD;AAC1E,EAAA,IAAI,CAAE,MAAM,iBAAA,EAAkB,EAAI,OAAO,IAAA;AAEzC,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,GAAA,CAAI,gBAAA;AAAA,EACf,SAAS,GAAA,EAAK;AACV,IAAA,GAAA,CAAI,KAAA,CAAM,mCAAmC,GAAG,CAAA;AAChD,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAaA,eAAsB,uBAClB,SAAA,EACgC;AAChC,EAAA,IAAI,CAAE,MAAM,iBAAA,EAAkB,EAAI,OAAO,IAAA;AAEzC,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,GAAA,CAAI,mBAAmB,SAAS,CAAA;AAAA,EAC3C,SAAS,GAAA,EAAK;AACV,IAAA,GAAA,CAAI,KAAA,CAAM,sCAAsC,GAAG,CAAA;AACnD,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAeA,eAAsB,gBAAgB,WAAA,EAA6D;AAC/F,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACA,IAAA,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AAAA,EACvD,SAAS,GAAA,EAAK;AAEV,IAAA,GAAA,CAAI,KAAA,CAAM,+BAA+B,GAAG,CAAA;AAC5C,IAAA,OAAO,IAAA;AAAA,EACX;AACA,EAAA,OAAO,mBAAA,CAAoB,KAAK,WAAW,CAAA;AAC/C;AAWA,eAAe,mBAAA,CACX,KACA,WAAA,EAC+B;AAI/B,EAAA,IAAI,CAAC,GAAA,CAAI,gBAAA,CAAiB,oBAAA,EAAqB,EAAG;AAC9C,IAAA,OAAO,IAAA;AAAA,EACX;AAMA,EAAA,IAAI,CAAE,MAAM,sBAAA,CAAuB,GAAA,EAAK,WAAW,CAAA,EAAI;AACnD,IAAA,MAAM,IAAI,uBAAuB,WAAW,CAAA;AAAA,EAChD;AAEA,EAAA,OAAO,GAAA,CAAI,mBAAmB,WAAW,CAAA;AAC7C;AASO,SAAS,qBAAA,GAAiC;AAC7C,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,KAAA;AAE1C,EAAA,MAAM,GAAA,GAAM,MAAA;AAGZ,EAAA,IAAI;AACA,IAAA,IAAI,MAAA,KAAW,MAAA,CAAO,GAAA,EAAK,OAAO,IAAA;AAAA,EACtC,CAAA,CAAA,MAAQ;AAEJ,IAAA,OAAO,IAAA;AAAA,EACX;AAGA,EAAA,IAAI,GAAA,CAAI,qBAAA,KAA0B,IAAA,EAAM,OAAO,IAAA;AAG/C,EAAA,IAAI,GAAA,CAAI,iBAAA,IAAqB,IAAA,EAAM,OAAO,IAAA;AAE1C,EAAA,OAAO,KAAA;AACX;AAWA,eAAsB,iBAAA,GAAwD;AAC1E,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,IAAI,oBAAA,EAAqB;AAAA,EACpC,SAAS,GAAA,EAAK;AACV,IAAA,GAAA,CAAI,KAAA,CAAM,iCAAiC,GAAG,CAAA;AAC9C,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;;;AC3NO,IAAM,aAAA,GAAgB;AAAA,EACzB,KAAA,EAAO,CAAC,2CAA2C,CAAA;AAAA,EACnD,MAAA,EAAQ,CAAC,uCAAuC,CAAA;AAAA,EAChD,UAAU,EAAC;AAAA,EACX,QAAQ;AACZ;AAGO,IAAM,yBAAA,GAAoC,aAAA,CAAc,KAAA,CAAM,CAAC;ACMtE,IAAMA,IAAAA,GAAMC,aAAa,MAAM,CAAA;AAYxB,SAAS,gBAAgB,GAAA,EAAsB;AAElD,EAAA,MAAM,KAAA,GAAQ,mBAAA,CAAoB,GAAG,CAAA,GAAI,IAAI,KAAA,GAAQ,GAAA;AAErD,EAAA,IAAI,KAAA,YAAiB,KAAA,EAAO,OAAO,KAAA,CAAM,OAAA;AACzC,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,KAAA;AACtC,EAAA,IACI,KAAA,IAAS,IAAA,IACT,OAAO,KAAA,KAAU,QAAA,IACjB,aAAa,KAAA,IACb,OAAQ,KAAA,CAA+B,OAAA,KAAY,QAAA,EACrD;AACE,IAAA,MAAM,KAAA,GAAQ,KAAA;AACd,IAAA,OAAO,OAAO,KAAA,CAAM,IAAA,KAAS,QAAA,GAAW,CAAA,EAAG,KAAA,CAAM,IAAI,CAAA,EAAA,EAAK,KAAA,CAAM,OAAO,CAAA,CAAA,GAAK,KAAA,CAAM,OAAA;AAAA,EACtF;AACA,EAAA,IAAI;AACA,IAAA,OAAO,IAAA,CAAK,UAAU,KAAK,CAAA;AAAA,EAC/B,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,OAAO,KAAK,CAAA;AAAA,EACvB;AACJ;AAEA,SAAS,oBAAoB,KAAA,EAA0D;AACnF,EAAA,OACI,KAAA,IAAS,IAAA,IACT,OAAO,KAAA,KAAU,QAAA,IACjB,KAAA,IAAS,KAAA,IACT,OAAA,IAAW,KAAA,IACX,OAAQ,KAAA,CAA2B,GAAA,KAAQ,QAAA;AAEnD;AA6EA,IAAI,YAAA,GAA8B,IAAA;AAkClC,eAAsB,SAAA,GAAoC;AACtD,EAAA,IAAI,cAAc,OAAO,YAAA;AAEzB,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,YAAA,GAAe,GAAA,CAAI,OAAA;AACnB,IAAAD,IAAAA,CAAI,MAAM,eAAe,CAAA;AACzB,IAAA,OAAO,YAAA;AAAA,EACX,CAAA,CAAA,MAAQ;AACJ,IAAAA,IAAAA,CAAI,MAAM,4DAA4D,CAAA;AACtE,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AA0BA,eAAsB,kBAAA,GAAsD;AACxE,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,GAAA,CAAI,eAAA;AAAA,EACf,SAAS,GAAA,EAAK;AACV,IAAAA,IAAAA,CAAI,KAAA,CAAM,gCAAA,EAAkC,GAAG,CAAA;AAC/C,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAqBA,eAAsB,0BAClB,SAAA,EAC+B;AAC/B,EAAA,IAAI,CAAE,MAAM,iBAAA,EAAkB,EAAI,OAAO,IAAA;AACzC,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,GAAA,CAAI,sBAAsB,SAAS,CAAA;AAAA,EAC9C,SAAS,GAAA,EAAK;AACV,IAAAA,IAAAA,CAAI,KAAA,CAAM,uCAAA,EAAyC,GAAG,CAAA;AACtD,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAOA,eAAsB,mBAAA,GAAwD;AAC1E,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,IAAI,sBAAA,EAAuB;AAAA,EACtC,SAAS,GAAA,EAAK;AACV,IAAAA,IAAAA,CAAI,KAAA,CAAM,iCAAA,EAAmC,GAAG,CAAA;AAChD,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAwDA,eAAsB,0BAClB,SAAA,EAC4B;AAC5B,EAAA,MAAM,MAAA,GAAS,MAAM,SAAA,EAAU;AAC/B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAM,IAAI,MAAM,+CAA+C,CAAA;AAAA,EACnE;AACA,EAAAA,IAAAA,CAAI,KAAA,CAAM,2BAAA,EAA6B,EAAE,SAAA,EAAW,SAAA,CAAU,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,GAAG,CAAA,EAAG,CAAA;AAGjF,EAAA,OAAO,MAAM,MAAA,CAAO,yBAAA,CAA0BE,UAAU,IAAA,EAAM,SAAS,CAAC,CAAA,CAAE,KAAA;AAAA,IACtE,CAAC,aAAwD,QAAA,CAAS,KAAA;AAAA,IAClE,CAAC,GAAA,KAAiB;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kCAAA,EAAqC,eAAA,CAAgB,GAAG,CAAC,CAAA,CAAA,EAAI;AAAA,QACzE,KAAA,EAAO;AAAA,OACV,CAAA;AAAA,IACL;AAAA,GACJ;AACJ;AA+CA,eAAsB,sBAAsB,SAAA,EAA+C;AACvF,EAAA,MAAM,MAAA,GAAS,MAAM,SAAA,EAAU;AAC/B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAM,IAAI,MAAM,2CAA2C,CAAA;AAAA,EAC/D;AACA,EAAAF,IAAAA,CAAI,MAAM,uBAAA,EAAyB;AAAA,IAC/B,MAAA,EAAQ,UAAU,MAAA,CAAO,MAAA;AAAA,IACzB,OAAA,EAAS,SAAA,CAAU,IAAA,EAAM,MAAA,IAAU;AAAA,GACtC,CAAA;AAGD,EAAA,OAAO,MAAM,MAAA,CAAO,mCAAA,CAAoCE,UAAU,IAAA,EAAM,SAAS,CAAC,CAAA,CAAE,KAAA;AAAA,IAChF,CAAC,aAAmD,QAAA,CAAS,KAAA;AAAA,IAC7D,CAAC,GAAA,KAAiB;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,8BAAA,EAAiC,eAAA,CAAgB,GAAG,CAAC,CAAA,CAAA,EAAI;AAAA,QACrE,KAAA,EAAO;AAAA,OACV,CAAA;AAAA,IACL;AAAA,GACJ;AACJ;ACrYA,IAAMF,IAAAA,GAAMC,aAAa,kBAAkB,CAAA;AAmC3C,eAAsB,kBAAkB,UAAA,EAAgD;AACpF,EAAA,MAAM,MAAA,GAAS,MAAM,SAAA,EAAU;AAC/B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,EAC3D;AACA,EAAAD,KAAI,KAAA,CAAM,mBAAA,EAAqB,EAAE,GAAA,EAAK,UAAA,CAAW,KAAK,CAAA;AAEtD,EAAA,OAAO,MAAM,MAAA,CAAO,UAAA,CAAWE,UAAU,IAAA,EAAM,UAAU,CAAC,CAAA,CAAE,KAAA;AAAA,IACxD,CAAC,aAA4C,QAAA,CAAS,KAAA;AAAA,IACtD,CAAC,GAAA,KAAiB;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,eAAA,CAAgB,GAAG,CAAC,CAAA,CAAA,EAAI,EAAE,KAAA,EAAO,GAAA,EAAK,CAAA;AAAA,IACvF;AAAA,GACJ;AACJ;AAqBA,eAAsB,wBAAwB,UAAA,EAAoD;AAC9F,EAAA,MAAM,MAAA,GAAS,MAAM,SAAA,EAAU;AAC/B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,EACjE;AACA,EAAAF,IAAAA,CAAI,KAAA,CAAM,yBAAA,EAA2B,EAAE,YAAY,CAAA;AAEnD,EAAA,OAAO,MAAM,MAAA,CAAO,gBAAA,CAAiBE,UAAU,IAAA,EAAM,UAAU,CAAC,CAAA,CAAE,KAAA;AAAA,IAC9D,CAAC,aAA4C,QAAA,CAAS,KAAA;AAAA,IACtD,CAAC,GAAA,KAAiB;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,gCAAA,EAAmC,eAAA,CAAgB,GAAG,CAAC,CAAA,CAAA,EAAI;AAAA,QACvE,KAAA,EAAO;AAAA,OACV,CAAA;AAAA,IACL;AAAA,GACJ;AACJ;AC/EA,IAAMF,IAAAA,GAAMC,aAAa,YAAY,CAAA;AA4DrC,eAAsB,gBAAA,GAAkD;AACpE,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,IAAI,mBAAA,EAAoB;AAAA,EACnC,SAAS,GAAA,EAAK;AACV,IAAAD,IAAAA,CAAI,KAAA,CAAM,8BAAA,EAAgC,GAAG,CAAA;AAC7C,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AC9EA,IAAMA,IAAAA,GAAMC,aAAa,cAAc,CAAA;AAoBvC,eAAsB,cAAc,GAAA,EAAsC;AACtE,EAAA,MAAM,MAAA,GAAS,MAAM,SAAA,EAAU;AAC/B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAM,IAAI,MAAM,mCAAmC,CAAA;AAAA,EACvD;AACA,EAAAD,KAAI,KAAA,CAAM,eAAA,EAAiB,EAAE,MAAA,EAAQ,GAAA,CAAI,QAAQ,CAAA;AAEjD,EAAA,OAAO,MAAM,MAAA,CAAO,aAAA,CAAcE,UAAU,IAAA,EAAM,GAAG,CAAC,CAAA,CAAE,KAAA;AAAA,IACpD,CAAC,aAA+C,QAAA,CAAS,KAAA;AAAA,IACzD,CAAC,GAAA,KAAiB;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,eAAA,CAAgB,GAAG,CAAC,CAAA,CAAA,EAAI,EAAE,KAAA,EAAO,GAAA,EAAK,CAAA;AAAA,IACnF;AAAA,GACJ;AACJ;ACtBA,IAAMF,IAAAA,GAAMC,aAAa,WAAW,CAAA;AAsDpC,eAAsB,cAAA,GAA8C;AAChE,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,IAAI,wBAAA,EAAyB;AAAA,EACxC,SAAS,GAAA,EAAK;AACV,IAAAD,IAAAA,CAAI,KAAA,CAAM,4BAAA,EAA8B,GAAG,CAAA;AAC3C,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAeO,SAAS,yBACZ,GAAA,EACyB;AACzB,EAAA,OAAO,CAAC,QAAQ,MAAA,KAAW;AACvB,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,MAAA,CAAO,WAAW,CAAA;AACvC,IAAA,IAAI,CAAC,QAAA,EAAU;AACX,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,MAAA,CAAO,WAAW,CAAA,eAAA,CAAiB,CAAA;AAAA,IACpF;AACA,IAAA,OAAO,QAAA,CAAS,QAAQ,MAAM,CAAA;AAAA,EAClC,CAAA;AACJ;ACpFA,IAAMA,IAAAA,GAAMC,aAAa,eAAe,CAAA;AA2CxC,eAAsB,iBAAA,GAAoD;AACtE,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,GAAA,CAAI,cAAA;AAAA,EACf,SAAS,GAAA,EAAK;AACV,IAAAD,IAAAA,CAAI,KAAA,CAAM,+BAAA,EAAiC,GAAG,CAAA;AAC9C,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;ACxDA,IAAMA,IAAAA,GAAMC,aAAa,oBAAoB,CAAA;AAmE7C,eAAsB,sBAAA,GAA8D;AAChF,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,gCAAgC,CAAA;AACzD,IAAA,OAAO,GAAA,CAAI,mBAAA;AAAA,EACf,SAAS,GAAA,EAAK;AACV,IAAAD,IAAAA,CAAI,KAAA,CAAM,oCAAA,EAAsC,GAAG,CAAA;AACnD,IAAA,OAAO,IAAA;AAAA,EACX;AACJ","file":"index.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport type { JsonRpcProvider } from \"polkadot-api\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { enumValue, type Transport } from \"@novasamatech/host-api\";\n\nimport type { HostLocalStorage, HostStatementStore } from \"./types.js\";\n\nconst log = createLogger(\"host:container\");\n\n/**\n * Thrown by {@link getHostProvider} when the host container is reachable but does\n * not support the requested chain — e.g. the chain isn't enabled in this host\n * build, or the descriptor's genesis hash has drifted from the host's after a\n * network reset.\n *\n * Surfacing this as a thrown error (rather than handing back a provider that\n * silently swallows every JSON-RPC request) is what lets callers of\n * `createChainClient` detect the failure. Without it, the host's fallback no-op\n * provider drops every request on the floor and queries await forever.\n */\nexport class ChainNotSupportedError extends Error {\n /** Genesis hash of the chain the host refused, for programmatic detection. */\n readonly genesisHash: string;\n\n constructor(genesisHash: string) {\n super(\n `Chain ${genesisHash} is not supported by the current host. It may not be enabled in this host build, or its genesis hash may have drifted after a network reset.`,\n );\n this.name = \"ChainNotSupportedError\";\n this.genesisHash = genesisHash;\n }\n}\n\n/**\n * Ask the host whether it can serve the given chain, using the same\n * `host_feature_supported` check the wrapper's provider performs internally\n * before it decides whether to start a real provider or a no-op one.\n *\n * @throws If the host connection never becomes ready, or the host rejects the\n * support check outright. Both are non-hanging, catchable failures.\n */\nasync function isChainSupportedByHost(\n sdk: typeof import(\"@novasamatech/host-api-wrapper\"),\n genesisHash: `0x${string}`,\n): Promise<boolean> {\n const ready = await sdk.sandboxTransport.isReady();\n if (!ready) {\n throw new Error(\n `Host connection did not become ready; cannot verify support for chain ${genesisHash}.`,\n );\n }\n const result = await sdk.hostApi.featureSupported(\n enumValue(\"v1\", enumValue(\"Chain\", genesisHash)),\n );\n return result.match(\n (ok) => ok.value === true,\n (err) => {\n // The reason lives at value.payload.reason for host-protocol errors and\n // value.reason for request-level ones; tolerate both against upstream drift.\n const value = (err as { value?: { payload?: { reason?: string }; reason?: string } })\n ?.value;\n const reason = value?.payload?.reason ?? value?.reason ?? \"unknown reason\";\n throw new Error(`Host rejected the chain-support check for ${genesisHash}: ${reason}`);\n },\n );\n}\n\n/**\n * Detect if running inside a Host container (Polkadot Browser / Polkadot Desktop).\n *\n * The SDK is designed to run exclusively inside a host container. This function\n * is primarily useful for early validation or informational purposes.\n *\n * Uses product-sdk's sandboxProvider as primary detection.\n * Falls back to manual signal checks when product-sdk is not installed.\n */\nexport async function isInsideContainer(): Promise<boolean> {\n if (typeof window === \"undefined\") return false;\n\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.sandboxProvider.isCorrectEnvironment();\n } catch {\n return isInsideContainerSync();\n }\n}\n\n/**\n * Get the Host API localStorage instance when running inside a container.\n * Returns null outside a container or when product-sdk is unavailable.\n */\nexport async function getHostLocalStorage(): Promise<HostLocalStorage | null> {\n if (!(await isInsideContainer())) return null;\n\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.hostLocalStorage as HostLocalStorage;\n } catch (err) {\n log.debug(\"getHostLocalStorage unavailable\", err);\n return null;\n }\n}\n\n/**\n * Construct a fresh host-backed `HostLocalStorage` instance with an optional\n * custom transport. Use this when you need a non-default transport (e.g.\n * for tests); otherwise prefer {@link getHostLocalStorage}, which returns\n * the shared singleton.\n *\n * Mirrors `createLocalStorage` from `@novasamatech/host-api-wrapper`.\n *\n * @param transport - Optional transport; defaults to the sandbox transport.\n * @returns A new `HostLocalStorage` instance, or `null` if unavailable.\n */\nexport async function createHostLocalStorage(\n transport?: Transport,\n): Promise<HostLocalStorage | null> {\n if (!(await isInsideContainer())) return null;\n\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createLocalStorage(transport);\n } catch (err) {\n log.debug(\"createHostLocalStorage unavailable\", err);\n return null;\n }\n}\n\n/**\n * Get a PAPI-compatible JSON-RPC provider that routes through the host connection.\n *\n * When running inside a Polkadot container, this wraps the chain connection via the\n * host's `createPapiProvider`, enabling shared connections and efficient routing.\n * Returns `null` when `@novasamatech/host-api-wrapper` is unavailable or when not\n * running inside a container.\n *\n * @param genesisHash - Genesis hash of the target chain (`0x`-prefixed hex string).\n * @returns A host-routed `JsonRpcProvider`, or `null` if unavailable.\n * @throws {ChainNotSupportedError} When inside a container but the host can't serve\n * the chain — surfaced instead of returning a provider that would hang forever.\n */\nexport async function getHostProvider(genesisHash: `0x${string}`): Promise<JsonRpcProvider | null> {\n let sdk: typeof import(\"@novasamatech/host-api-wrapper\");\n try {\n sdk = await import(\"@novasamatech/host-api-wrapper\");\n } catch (err) {\n // Wrapper not installed — we're not running inside a container.\n log.debug(\"getHostProvider unavailable\", err);\n return null;\n }\n return resolveHostProvider(sdk, genesisHash);\n}\n\n/**\n * Decide whether to build a host provider for `genesisHash`, given the resolved\n * wrapper module. Split out of {@link getHostProvider} so the decision logic can\n * be unit-tested with a fake wrapper, without re-importing the real\n * (browser-only) module.\n *\n * @returns the provider, or `null` when not inside a container.\n * @throws {ChainNotSupportedError} when the host can't serve the chain.\n */\nasync function resolveHostProvider(\n sdk: typeof import(\"@novasamatech/host-api-wrapper\"),\n genesisHash: `0x${string}`,\n): Promise<JsonRpcProvider | null> {\n // Outside a host container there is no provider to hand back. Mirrors\n // createPapiProvider's own environment guard; callers treat null as\n // \"not inside a container\".\n if (!sdk.sandboxTransport.isCorrectEnvironment()) {\n return null;\n }\n\n // Inside a container: confirm the host can actually serve this chain before\n // handing PAPI a provider. When the host doesn't support the chain, the\n // wrapper's fallback provider silently swallows every JSON-RPC request and\n // the caller hangs forever with no rejection. Surface a catchable error.\n if (!(await isChainSupportedByHost(sdk, genesisHash))) {\n throw new ChainNotSupportedError(genesisHash);\n }\n\n return sdk.createPapiProvider(genesisHash);\n}\n\n/**\n * Synchronous container detection — fast heuristic check without product-sdk.\n *\n * Checks for iframe, webview marker, and host message port signals.\n * Use this when you need a quick sync check (e.g., in hot code paths).\n * For full detection including product-sdk, use {@link isInsideContainer} (async).\n */\nexport function isInsideContainerSync(): boolean {\n if (typeof window === \"undefined\") return false;\n\n const win = window as unknown as Record<string, unknown>;\n\n // Iframe detection (polkadot.com browser)\n try {\n if (window !== window.top) return true;\n } catch {\n // Cross-origin iframe — likely inside a container\n return true;\n }\n\n // Webview detection (Polkadot Desktop)\n if (win.__HOST_WEBVIEW_MARK__ === true) return true;\n\n // Desktop message-passing API\n if (win.__HOST_API_PORT__ != null) return true;\n\n return false;\n}\n\n/**\n * Get the host API statement store when running inside a container.\n *\n * Returns a statement store with `subscribe`, `createProof`, and `submit` methods\n * that communicate through the host's native binary protocol — bypassing JSON-RPC\n * entirely. Returns `null` when `@novasamatech/host-api-wrapper` is unavailable.\n *\n * @returns The host statement store, or `null` if unavailable.\n */\nexport async function getStatementStore(): Promise<HostStatementStore | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createStatementStore() as HostStatementStore;\n } catch (err) {\n log.debug(\"getStatementStore unavailable\", err);\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, vi } = import.meta.vitest;\n\n // A self-contained stand-in for the host wrapper, so the chain-support\n // decision can be tested without re-importing the real (browser-only) module.\n const fakeProvider = (() => {}) as unknown as JsonRpcProvider;\n function makeFakeSdk(opts: {\n inContainer?: boolean;\n ready?: boolean;\n supported?: boolean;\n featureErr?: string | null;\n onCreate?: (genesisHash: string) => void;\n }) {\n const { inContainer = true, ready = true, supported = true, featureErr = null } = opts;\n return {\n sandboxTransport: {\n isCorrectEnvironment: () => inContainer,\n isReady: async () => ready,\n },\n hostApi: {\n featureSupported: (_payload: unknown) => ({\n match: (\n okFn: (ok: { tag: string; value: boolean }) => boolean,\n errFn: (err: { value: { payload: { reason: string } } }) => boolean,\n ) =>\n featureErr\n ? errFn({ value: { payload: { reason: featureErr } } })\n : okFn({ tag: \"v1\", value: supported }),\n }),\n },\n createPapiProvider: (genesisHash: string) => {\n opts.onCreate?.(genesisHash);\n return fakeProvider;\n },\n } as unknown as typeof import(\"@novasamatech/host-api-wrapper\");\n }\n\n test(\"returns false in Node environment (no window)\", async () => {\n expect(await isInsideContainer()).toBe(false);\n });\n\n test(\"manualDetection returns true for __HOST_WEBVIEW_MARK__\", async () => {\n const fakeWindow = {\n top: null,\n __HOST_WEBVIEW_MARK__: true,\n };\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true for __HOST_API_PORT__\", async () => {\n const fakeWindow = {\n top: null,\n __HOST_API_PORT__: 12345,\n };\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns false when no signals present\", async () => {\n const fakeWindow = { top: null };\n Object.defineProperty(fakeWindow, \"top\", { get: () => fakeWindow });\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(false);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true for cross-origin iframe\", async () => {\n const fakeWindow = {};\n Object.defineProperty(fakeWindow, \"top\", {\n get: () => {\n throw new DOMException(\"cross-origin\");\n },\n });\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true when window !== window.top (iframe)\", async () => {\n const fakeWindow = { top: {} }; // top is a different object\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"getHostLocalStorage returns null outside container\", async () => {\n expect(await getHostLocalStorage()).toBeNull();\n });\n\n test(\"createHostLocalStorage returns null outside container\", async () => {\n expect(await createHostLocalStorage()).toBeNull();\n });\n\n // --- chain-support gating (resolveHostProvider) ---\n\n test(\"resolves to the provider when supported, and null outside a container\", async () => {\n const created: string[] = [];\n const onCreate = (g: string) => created.push(g);\n\n // Inside a container, supported chain -> real provider.\n expect(await resolveHostProvider(makeFakeSdk({ onCreate }), \"0xabc\")).toBe(fakeProvider);\n // Outside a container -> null, without constructing a provider.\n expect(\n await resolveHostProvider(makeFakeSdk({ inContainer: false, onCreate }), \"0xdef\"),\n ).toBeNull();\n\n expect(created).toEqual([\"0xabc\"]);\n });\n\n test.each([\n { when: \"the host doesn't support the chain\", opts: { supported: false } },\n { when: \"the host connection never becomes ready\", opts: { ready: false } },\n ])(\"throws (and never builds a provider) when $when\", async ({ opts }) => {\n const created: string[] = [];\n const sdk = makeFakeSdk({ ...opts, onCreate: (g) => created.push(g) });\n await expect(resolveHostProvider(sdk, \"0xabc\")).rejects.toThrow();\n // Crucially: no provider is created, so PAPI never receives a hanging no-op.\n expect(created).toEqual([]);\n });\n\n test(\"unsupported chains throw a ChainNotSupportedError carrying the genesis hash\", async () => {\n const err = await resolveHostProvider(makeFakeSdk({ supported: false }), \"0xfeed\").catch(\n (e) => e,\n );\n expect(err).toBeInstanceOf(ChainNotSupportedError);\n expect((err as ChainNotSupportedError).genesisHash).toBe(\"0xfeed\");\n });\n\n test(\"getStatementStore returns null when product-sdk unavailable\", async () => {\n const result = await getStatementStore();\n expect(result).toBeNull();\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Shared chain network configuration — single source of truth for\n * chain-specific endpoints used by multiple packages.\n */\n\n/**\n * Bulletin Chain RPC endpoints per network environment. `paseo` and `summit`\n * are populated today; `polkadot` and `kusama` are reserved for when those\n * Bulletin deployments go live.\n */\nexport const BULLETIN_RPCS = {\n paseo: [\"wss://paseo-bulletin-next-rpc.polkadot.io\"],\n summit: [\"wss://summit-bulletin-rpc.polkadot.io\"],\n polkadot: [] as string[],\n kusama: [] as string[],\n} as const;\n\n/** Default Bulletin Chain endpoint — the first entry under {@link BULLETIN_RPCS}.paseo. */\nexport const DEFAULT_BULLETIN_ENDPOINT: string = BULLETIN_RPCS.paseo[0];\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"chains config\", () => {\n test(\"BULLETIN_RPCS has paseo endpoint\", () => {\n expect(BULLETIN_RPCS.paseo.length).toBeGreaterThan(0);\n expect(BULLETIN_RPCS.paseo[0]).toMatch(/^wss:\\/\\//);\n });\n\n test(\"BULLETIN_RPCS has summit endpoint\", () => {\n expect(BULLETIN_RPCS.summit.length).toBeGreaterThan(0);\n expect(BULLETIN_RPCS.summit[0]).toMatch(/^wss:\\/\\//);\n });\n\n test(\"BULLETIN_RPCS polkadot and kusama are empty until live\", () => {\n expect(BULLETIN_RPCS.polkadot).toEqual([]);\n expect(BULLETIN_RPCS.kusama).toEqual([]);\n });\n\n test(\"DEFAULT_BULLETIN_ENDPOINT matches first paseo endpoint\", () => {\n expect(DEFAULT_BULLETIN_ENDPOINT).toBe(BULLETIN_RPCS.paseo[0]);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * TruAPI - the protocol for communicating between apps and the Polkadot host container.\n *\n * This module centralizes access to @novasamatech/host-api-wrapper and @novasamatech/host-api,\n * allowing other @parity/product-sdk-* packages to import from here rather than depending\n * directly on novasama packages.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { enumValue } from \"@novasamatech/host-api\";\nimport type {\n AllocatableResource as AllocatableResourceCodec,\n AllocationOutcome as AllocationOutcomeCodec,\n CodecType,\n RemotePermission as RemotePermissionCodec,\n} from \"@novasamatech/host-api\";\nimport type { createAccountsProvider, preimageManager } from \"@novasamatech/host-api-wrapper\";\n\nimport { isInsideContainer } from \"./container.js\";\nimport type { Statement, StatementProof } from \"./types.js\";\n\nconst log = createLogger(\"host\");\n\n/**\n * Extract a human-readable message from a host-side error. Hosts wrap\n * errors in versioned envelopes (`{ tag: \"v1\", value: CodecError }`); this\n * helper unwraps the envelope and renders the inner error's `name`/`message`\n * so callers see the host's actual diagnostic instead of `\"[object Object]\"`\n * (from `String(err)`) or a JSON-stringified envelope.\n *\n * Exported for the higher-level wrappers (`requestPermission`,\n * `deriveEntropy`, etc.) that build their `throw new Error(...)` messages.\n */\nexport function formatHostError(err: unknown): string {\n // Single-level unwrap only; nested envelopes fall through to JSON.stringify.\n const inner = isVersionedEnvelope(err) ? err.value : err;\n\n if (inner instanceof Error) return inner.message;\n if (typeof inner === \"string\") return inner;\n if (\n inner != null &&\n typeof inner === \"object\" &&\n \"message\" in inner &&\n typeof (inner as { message: unknown }).message === \"string\"\n ) {\n const named = inner as { name?: unknown; message: string };\n return typeof named.name === \"string\" ? `${named.name}: ${named.message}` : named.message;\n }\n try {\n return JSON.stringify(inner);\n } catch {\n return String(inner);\n }\n}\n\nfunction isVersionedEnvelope(value: unknown): value is { tag: string; value: unknown } {\n return (\n value != null &&\n typeof value === \"object\" &&\n \"tag\" in value &&\n \"value\" in value &&\n typeof (value as { tag: unknown }).tag === \"string\"\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Helpers from @novasamatech/host-api (re-exported from @novasamatech/scale)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport {\n /**\n * Construct an enum variant for TruAPI calls.\n *\n * @example\n * ```ts\n * import { enumValue, getTruApi } from \"@parity/product-sdk-host\";\n *\n * const truApi = await getTruApi();\n * if (truApi) {\n * await truApi.permission([enumValue(\"ChainSubmit\")]);\n * }\n * ```\n */\n enumValue,\n /**\n * Check if a value is a specific enum variant.\n */\n isEnumVariant,\n /**\n * Assert that a value is a specific enum variant, throwing if not.\n */\n assertEnumVariant,\n /**\n * Unwrap a Result, throwing on error.\n */\n unwrapResultOrThrow,\n /**\n * Create an Ok result.\n */\n resultOk,\n /**\n * Create an Err result.\n */\n resultErr,\n /**\n * Convert bytes to hex string.\n */\n toHex,\n /**\n * Convert hex string to bytes.\n */\n fromHex,\n} from \"@novasamatech/host-api\";\n\n/** A `0x`-prefixed hex string (the template literal type ``\\`0x${string}\\``) used by the host API surface for raw byte payloads. Re-exported from `@novasamatech/host-api` so consumers bridging between host APIs and SDK code can reach the host-side type without an additional dependency. */\nexport type { HexString } from \"@novasamatech/host-api\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// TruAPI accessor\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The TruApi type - provides low-level methods for communicating with the host.\n *\n * Methods include:\n * - `navigateTo(url)` - Navigate to a URL within the host\n * - `permission(permissions)` - Request permissions from the host\n * - `localStorageRead/Write/Clear` - Host-backed storage\n * - `sign(payload)` - Request transaction signing\n * - `deriveEntropy(context)` - Derive deterministic entropy\n * - `themeSubscribe()` - Subscribe to host theme changes\n * - And many more...\n *\n * Type identical to `hostApi` from `@novasamatech/host-api-wrapper` so that\n * `truApi.X(...)` calls keep their full inference (return types, method\n * names, parameter shapes) instead of decaying to `any`.\n */\nexport type TruApi = typeof import(\"@novasamatech/host-api-wrapper\").hostApi;\n\n/** Cached TruApi instance */\nlet cachedTruApi: TruApi | null = null;\n\n/**\n * Get the TruAPI instance for direct low-level access.\n *\n * Returns the `hostApi` object from `@novasamatech/host-api-wrapper` which provides\n * methods for communicating directly with the host container. Returns `null`\n * when running outside a container or when the SDK is unavailable.\n *\n * For most use cases, prefer the higher-level functions like `getHostLocalStorage()`,\n * `getHostProvider()`, etc. Use this when you need direct access to host methods\n * like `navigateTo()`, `permission()`, or `deriveEntropy()`.\n *\n * @example\n * ```ts\n * import { getTruApi, enumValue } from \"@parity/product-sdk-host\";\n *\n * const truApi = await getTruApi();\n * if (truApi) {\n * // Request permission\n * const result = await truApi.permission([enumValue(\"ChainSubmit\")]);\n *\n * // Navigate to a URL\n * await truApi.navigateTo(\"polkadot://settings\");\n *\n * // Subscribe to theme changes\n * const sub = truApi.themeSubscribe(undefined, (theme) => {\n * console.log(\"Theme changed:\", theme);\n * });\n * }\n * ```\n *\n * @returns The TruAPI instance, or `null` if unavailable.\n */\nexport async function getTruApi(): Promise<TruApi | null> {\n if (cachedTruApi) return cachedTruApi;\n\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n cachedTruApi = sdk.hostApi;\n log.debug(\"TruAPI loaded\");\n return cachedTruApi;\n } catch {\n log.debug(\"TruAPI unavailable (not in container or SDK not installed)\");\n return null;\n }\n}\n\n/**\n * Get the preimage manager for bulletin chain operations.\n *\n * The preimage manager handles uploading and looking up preimages (arbitrary data)\n * on the bulletin chain through the host's optimized path.\n *\n * @returns The preimage manager, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getPreimageManager } from \"@parity/product-sdk-host\";\n *\n * const manager = await getPreimageManager();\n * if (manager) {\n * // Submit a preimage\n * const key = await manager.submit(new Uint8Array([1, 2, 3]));\n *\n * // Look up a preimage\n * const sub = manager.lookup(key, (data) => {\n * if (data) console.log(\"Found:\", data);\n * });\n * }\n * ```\n */\nexport async function getPreimageManager(): Promise<PreimageManager | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.preimageManager;\n } catch (err) {\n log.debug(\"getPreimageManager unavailable\", err);\n return null;\n }\n}\n\n/**\n * Preimage manager handle for bulletin chain operations. `lookup` returns a\n * `Subscription<void>` (`unsubscribe` + `onInterrupt`); `submit` returns a\n * `0x`-prefixed hex preimage key.\n *\n * Type identical to `preimageManager` from `@novasamatech/host-api-wrapper`.\n */\nexport type PreimageManager = typeof preimageManager;\n\n/**\n * Construct a fresh `PreimageManager` instance with an optional custom\n * transport. Use this when you need a non-default transport; otherwise\n * prefer {@link getPreimageManager}, which returns the shared singleton.\n *\n * Mirrors `createPreimageManager` from `@novasamatech/host-api-wrapper`.\n *\n * @param transport - Optional transport; defaults to the sandbox transport.\n * @returns A new `PreimageManager` instance, or `null` if unavailable.\n */\nexport async function createHostPreimageManager(\n transport?: import(\"@novasamatech/host-api\").Transport,\n): Promise<PreimageManager | null> {\n if (!(await isInsideContainer())) return null;\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createPreimageManager(transport);\n } catch (err) {\n log.debug(\"createHostPreimageManager unavailable\", err);\n return null;\n }\n}\n\n/**\n * Get the accounts provider for managing host accounts.\n *\n * @returns The accounts provider, or `null` if unavailable.\n */\nexport async function getAccountsProvider(): Promise<AccountsProvider | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createAccountsProvider();\n } catch (err) {\n log.debug(\"getAccountsProvider unavailable\", err);\n return null;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Resource allocation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Resource types requestable via {@link requestResourceAllocation}.\n * Derived from the upstream codec so variant renames surface as compile\n * errors, not runtime failures.\n */\nexport type AllocatableResource = CodecType<typeof AllocatableResourceCodec>;\n\n/** Tag-only view of {@link AllocatableResource} for places that just need the variant name. */\nexport type AllocatableResourceTag = AllocatableResource[\"tag\"];\n\n/**\n * Per-resource outcome from {@link requestResourceAllocation}.\n * The host strips secret payloads from `Allocated` before returning, so\n * `value` is always `undefined` on the product side.\n */\nexport type AllocationOutcome = CodecType<typeof AllocationOutcomeCodec>;\n\n/** Tag-only view of {@link AllocationOutcome} (`\"Allocated\" | \"Rejected\" | \"NotAvailable\"`). */\nexport type AllocationOutcomeTag = AllocationOutcome[\"tag\"];\n\n/**\n * Remote permission the dapp can ask the host to grant via\n * {@link requestPermission}.\n *\n * Derived from the upstream codec so variant renames surface as compile\n * errors, not runtime failures.\n */\nexport type RemotePermission = CodecType<typeof RemotePermissionCodec>;\n\n/** Tag-only view of {@link RemotePermission}. */\nexport type RemotePermissionTag = RemotePermission[\"tag\"];\n\n/**\n * Request the host to pre-allocate one or more resource allowances.\n *\n * The host prompts the user once; subsequent operations covered by the\n * granted allowance don't re-prompt.\n *\n * @param resources - Resources to request.\n * @returns Per-resource outcomes in the same order as `resources`.\n * @throws If the host is unavailable or the request fails.\n *\n * @example\n * ```ts\n * const outcomes = await requestResourceAllocation([\n * { tag: \"BulletinAllowance\", value: undefined },\n * ]);\n * if (outcomes[0].tag === \"Allocated\") { ... }\n * ```\n */\nexport async function requestResourceAllocation(\n resources: AllocatableResource[],\n): Promise<AllocationOutcome[]> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"requestResourceAllocation: TruAPI unavailable\");\n }\n log.debug(\"requestResourceAllocation\", { resources: resources.map((r) => r.tag) });\n\n // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.\n return await truApi.requestResourceAllocation(enumValue(\"v1\", resources)).match(\n (envelope: { tag: \"v1\"; value: AllocationOutcome[] }) => envelope.value,\n (err: unknown) => {\n throw new Error(`requestResourceAllocation failed: ${formatHostError(err)}`, {\n cause: err,\n });\n },\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Authorized Statement Store proof creation (RFC-10 §\"Statement Store allowance\")\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Have the host sign a Statement using an allowance-bearing account it\n * picks internally — RFC-10 §\"Statement Store allowance\".\n *\n * The product passes only the Statement payload; the host chooses the\n * `//allowance//statement-store//{productId}` account that holds SSS\n * allowance and signs with it. Allowance is provisioned implicitly on\n * first use if the host hasn't already pre-allocated via\n * {@link requestResourceAllocation}; products never see the signing\n * account or its key material.\n *\n * Pairs with {@link getStatementStore}'s `submit`: call this to obtain\n * a proof, attach it to the Statement, and submit the result.\n *\n * @param statement - The Statement to be signed.\n * @returns The proof to attach before submitting.\n * @throws If the host is unavailable or the host-side signing fails.\n *\n * @example\n * ```ts\n * import { createProofAuthorized, getStatementStore } from \"@parity/product-sdk-host\";\n *\n * const statement = {\n * proof: undefined,\n * decryptionKey: undefined,\n * expiry: undefined,\n * channel: undefined,\n * topics: [],\n * data: payload,\n * };\n * const proof = await createProofAuthorized(statement);\n * const store = await getStatementStore();\n * await store?.submit({ ...statement, proof });\n * ```\n *\n * @remarks\n * RFC-10 introduces this as a new, strictly additive TruAPI call. The\n * pre-existing `HostStatementStore.createProof(accountId, statement)`\n * surface stays available for products that own a non-allowance signing\n * account; this wrapper is the sponsored-submission path.\n */\nexport async function createProofAuthorized(statement: Statement): Promise<StatementProof> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"createProofAuthorized: TruAPI unavailable\");\n }\n log.debug(\"createProofAuthorized\", {\n topics: statement.topics.length,\n dataLen: statement.data?.length ?? 0,\n });\n\n // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.\n return await truApi.statementStoreCreateProofAuthorized(enumValue(\"v1\", statement)).match(\n (envelope: { tag: \"v1\"; value: StatementProof }) => envelope.value,\n (err: unknown) => {\n throw new Error(`createProofAuthorized failed: ${formatHostError(err)}`, {\n cause: err,\n });\n },\n );\n}\n\n/**\n * One of the user's existing wallet accounts, surfaced through the host and\n * identified by its public key and an optional name. Contrast with\n * {@link ProductAccount}, which is also user-controlled but derived by the\n * host for a specific app rather than picked from the user's existing keys.\n */\nexport interface HostAccount {\n publicKey: Uint8Array;\n name?: string;\n}\n\n/**\n * A product account — an app-scoped derived account managed by the host wallet.\n *\n * The host derives a unique keypair for each app (identified by `dotNsIdentifier`)\n * so apps get their own account that the user controls but is scoped to the app.\n */\nexport interface ProductAccount {\n /** App identifier (e.g., \"mark3t.dot\"). */\n dotNsIdentifier: string;\n /** Derivation index within the app scope. Default: 0 */\n derivationIndex: number;\n /** Raw public key (32 bytes). */\n publicKey: Uint8Array;\n}\n\n/**\n * A contextual alias obtained from Ring VRF.\n *\n * Proves account membership in a ring without revealing which account.\n */\nexport interface ContextualAlias {\n /** Ring context (32 bytes). */\n context: Uint8Array;\n /** The Ring VRF alias bytes. */\n alias: Uint8Array;\n}\n\n/**\n * Neverthrow-style ResultAsync returned by product-sdk methods.\n *\n * Use `.match(onOk, onErr)` to handle success/error cases.\n */\nexport interface ResultAsync<T, E> {\n match: <A, B = A>(ok: (t: T) => A, err: (e: E) => B) => Promise<A | B>;\n}\n\n/**\n * Accounts provider handle from `@novasamatech/host-api-wrapper`. Surfaces the\n * full upstream API - host wallet accounts, app-scoped product accounts,\n * Ring VRF, user identity (`getUserId`, `requestLogin`), and connection\n * status subscription.\n *\n * Type identical to `createAccountsProvider()` from\n * `@novasamatech/host-api-wrapper`; methods return neverthrow `ResultAsync`\n * values with typed `CodecError` variants in the error channel.\n */\nexport type AccountsProvider = ReturnType<typeof createAccountsProvider>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Tests\n// ─────────────────────────────────────────────────────────────────────────────\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getTruApi returns TruApi when SDK is available\", async () => {\n // Reset cache for test\n cachedTruApi = null;\n const api = await getTruApi();\n // In dev/test mode, product-sdk is installed\n expect(api === null || typeof api === \"object\").toBe(true);\n });\n\n test(\"getPreimageManager returns manager when SDK is available\", async () => {\n const manager = await getPreimageManager();\n // In dev/test mode, product-sdk is installed\n expect(manager === null || typeof manager === \"object\").toBe(true);\n });\n\n test(\"createHostPreimageManager returns null outside container\", async () => {\n expect(await createHostPreimageManager()).toBeNull();\n });\n\n test(\"formatHostError unwraps versioned envelopes and renders CodecError\", () => {\n expect(\n formatHostError({\n tag: \"v1\",\n value: { name: \"GenericError\", message: \"boom\" },\n }),\n ).toBe(\"GenericError: boom\");\n expect(formatHostError(new Error(\"plain\"))).toBe(\"plain\");\n expect(formatHostError(\"string err\")).toBe(\"string err\");\n expect(formatHostError({ tag: \"v1\", value: { message: \"no-name\" } })).toBe(\"no-name\");\n });\n\n test(\"getAccountsProvider returns provider when SDK is available\", async () => {\n // In dev/test mode, product-sdk is installed, so this returns a provider\n const provider = await getAccountsProvider();\n // Just verify it returns something (null when SDK unavailable, provider when available)\n expect(provider === null || typeof provider === \"object\").toBe(true);\n });\n\n test(\"enumValue is exported\", async () => {\n const { enumValue } = await import(\"./truapi.js\");\n expect(typeof enumValue).toBe(\"function\");\n });\n\n test(\"requestResourceAllocation throws when TruAPI is unavailable\", async () => {\n cachedTruApi = null;\n const api = await getTruApi();\n if (api === null) {\n await expect(\n requestResourceAllocation([{ tag: \"BulletinAllowance\", value: undefined }]),\n ).rejects.toThrow(/TruAPI unavailable/);\n } else {\n expect(typeof requestResourceAllocation).toBe(\"function\");\n }\n });\n\n test(\"createProofAuthorized throws when TruAPI is unavailable\", async () => {\n cachedTruApi = null;\n const api = await getTruApi();\n if (api === null) {\n await expect(\n createProofAuthorized({\n proof: undefined,\n decryptionKey: undefined,\n expiry: undefined,\n channel: undefined,\n topics: [],\n data: undefined,\n }),\n ).rejects.toThrow(/TruAPI unavailable/);\n } else {\n expect(typeof createProofAuthorized).toBe(\"function\");\n }\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Higher-level wrappers for the host's single-permission flows.\n *\n * `hostApi.permission` / `hostApi.devicePermission` take a versioned\n * envelope (`enumValue(\"v1\", ...)`) and return a neverthrow `ResultAsync`\n * of an unwrapped versioned response. Consumers rebuild that wrap/unwrap\n * dance every time. {@link requestPermission} and\n * {@link requestDevicePermission} collapse it to one-liners that match the\n * shape of {@link requestResourceAllocation} (throws on error, returns\n * the unwrapped payload on success).\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type { CodecType } from \"@novasamatech/host-api\";\nimport type { DevicePermission as DevicePermissionCodec } from \"@novasamatech/host-api\";\n\nimport { enumValue, formatHostError, getTruApi, type RemotePermission } from \"./truapi.js\";\n\nconst log = createLogger(\"host:permissions\");\n\n/**\n * Device permission the dapp can ask the host to grant via\n * {@link requestDevicePermission}.\n *\n * Derived from the upstream codec so variant renames surface as compile\n * errors, not runtime failures.\n */\nexport type DevicePermissionKind = CodecType<typeof DevicePermissionCodec>;\n\n/**\n * Alias of {@link RemotePermission} matching the upstream\n * `@novasamatech/host-api-wrapper` name. Use either freely.\n */\nexport type RemotePermissionItem = RemotePermission;\n\n/**\n * Request a single remote permission from the host.\n *\n * Builds the `v1` envelope, calls `hostApi.permission`, unwraps the response,\n * and returns the host's boolean granted/denied outcome.\n *\n * @param permission - The remote permission to request.\n * @returns `true` if the host granted the permission, `false` if denied.\n * @throws If the host is unavailable or the request fails.\n *\n * @example\n * ```ts\n * const granted = await requestPermission({ tag: \"ChainSubmit\", value: undefined });\n * if (!granted) {\n * tellUserToReconnect();\n * }\n * ```\n */\nexport async function requestPermission(permission: RemotePermission): Promise<boolean> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"requestPermission: TruAPI unavailable\");\n }\n log.debug(\"requestPermission\", { tag: permission.tag });\n\n return await truApi.permission(enumValue(\"v1\", permission)).match(\n (envelope: { tag: \"v1\"; value: boolean }) => envelope.value,\n (err: unknown) => {\n throw new Error(`requestPermission failed: ${formatHostError(err)}`, { cause: err });\n },\n );\n}\n\n/**\n * Request a single device permission (camera, microphone, etc.) from the\n * host.\n *\n * Builds the `v1` envelope, calls `hostApi.devicePermission`, unwraps the\n * response, and returns the host's boolean granted/denied outcome.\n *\n * @param permission - The device permission to request.\n * @returns `true` if the host granted the permission, `false` if denied.\n * @throws If the host is unavailable or the request fails.\n *\n * @example\n * ```ts\n * const granted = await requestDevicePermission(\"Camera\");\n * if (!granted) {\n * showCameraDeniedMessage();\n * }\n * ```\n */\nexport async function requestDevicePermission(permission: DevicePermissionKind): Promise<boolean> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"requestDevicePermission: TruAPI unavailable\");\n }\n log.debug(\"requestDevicePermission\", { permission });\n\n return await truApi.devicePermission(enumValue(\"v1\", permission)).match(\n (envelope: { tag: \"v1\"; value: boolean }) => envelope.value,\n (err: unknown) => {\n throw new Error(`requestDevicePermission failed: ${formatHostError(err)}`, {\n cause: err,\n });\n },\n );\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe, vi } = import.meta.vitest;\n\n async function withMockedTruApi<T>(\n bridge: {\n permission?: (req: unknown) => unknown;\n devicePermission?: (req: unknown) => unknown;\n } | null,\n fn: (mod: typeof import(\"./permissions.js\")) => Promise<T>,\n ): Promise<T> {\n vi.resetModules();\n vi.doMock(\"./truapi.js\", async (importOriginal) => {\n const original = await importOriginal<typeof import(\"./truapi.js\")>();\n return {\n ...original,\n getTruApi: async () => bridge,\n enumValue: (version: string, value: unknown) => ({ tag: version, value }),\n };\n });\n try {\n const mod = await import(\"./permissions.js\");\n return await fn(mod);\n } finally {\n vi.doUnmock(\"./truapi.js\");\n vi.resetModules();\n }\n }\n\n describe(\"requestPermission\", () => {\n test(\"throws when TruAPI is unavailable\", async () => {\n await withMockedTruApi(null, async (mod) => {\n await expect(\n mod.requestPermission({ tag: \"ChainSubmit\", value: undefined }),\n ).rejects.toThrow(/TruAPI unavailable/);\n });\n });\n\n test(\"unwraps the v1 boolean outcome\", async () => {\n await withMockedTruApi(\n {\n permission: vi.fn().mockReturnValue({\n match: async (onOk: (v: unknown) => unknown) =>\n onOk({ tag: \"v1\", value: true }),\n }),\n },\n async (mod) => {\n const granted = await mod.requestPermission({\n tag: \"ChainSubmit\",\n value: undefined,\n });\n expect(granted).toBe(true);\n },\n );\n });\n\n test(\"wraps host errors with a diagnostic message\", async () => {\n await withMockedTruApi(\n {\n permission: vi.fn().mockReturnValue({\n match: async (\n _onOk: (v: unknown) => unknown,\n onErr: (e: unknown) => unknown,\n ) =>\n onErr({\n tag: \"v1\",\n value: { name: \"GenericError\", message: \"boom\" },\n }),\n }),\n },\n async (mod) => {\n await expect(\n mod.requestPermission({ tag: \"ChainSubmit\", value: undefined }),\n ).rejects.toThrow(/requestPermission failed: GenericError: boom/);\n },\n );\n });\n });\n\n describe(\"requestDevicePermission\", () => {\n test(\"throws when TruAPI is unavailable\", async () => {\n await withMockedTruApi(null, async (mod) => {\n await expect(mod.requestDevicePermission(\"Camera\")).rejects.toThrow(\n /TruAPI unavailable/,\n );\n });\n });\n\n test(\"unwraps the v1 boolean outcome\", async () => {\n await withMockedTruApi(\n {\n devicePermission: vi.fn().mockReturnValue({\n match: async (onOk: (v: unknown) => unknown) =>\n onOk({ tag: \"v1\", value: true }),\n }),\n },\n async (mod) => {\n const granted = await mod.requestDevicePermission(\"Camera\");\n expect(granted).toBe(true);\n },\n );\n });\n\n test(\"wraps host errors with a diagnostic message\", async () => {\n await withMockedTruApi(\n {\n devicePermission: vi.fn().mockReturnValue({\n match: async (\n _onOk: (v: unknown) => unknown,\n onErr: (e: unknown) => unknown,\n ) =>\n onErr({\n tag: \"v1\",\n value: { name: \"GenericError\", message: \"boom\" },\n }),\n }),\n },\n async (mod) => {\n await expect(mod.requestDevicePermission(\"Camera\")).rejects.toThrow(\n /requestDevicePermission failed: GenericError: boom/,\n );\n },\n );\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Higher-level wrapper for the host's theme subscription.\n *\n * `hostApi.themeSubscribe` is reachable via {@link getTruApi}, but consumers\n * have to wire the subscription envelope themselves. `getThemeProvider`\n * returns the `@novasamatech/host-api-wrapper` theme provider object directly,\n * giving callers a `subscribeTheme(cb)` method that resolves to a typed\n * {@link ThemeMode} — a `{ name, variant }` struct where `variant` is\n * `\"Light\" | \"Dark\"` — and yields a `Subscription<void>` handle.\n *\n * @remarks\n * As of `host-api(-wrapper)` v0.8 the theme payload is a struct, not a flat\n * `\"light\" | \"dark\"` string: read {@link ThemeMode.variant} for the\n * light/dark value (now capitalized) and {@link ThemeMode.name} for the\n * active theme name (`Default`, or `Custom` carrying a string id).\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type {\n createThemeProvider,\n ThemeMode as NovasamaThemeMode,\n} from \"@novasamatech/host-api-wrapper\";\n\nconst log = createLogger(\"host:theme\");\n\n/**\n * Host theme provider handle. Exposes `subscribeTheme(callback)` which\n * receives a typed {@link ThemeMode} struct on every change and returns a\n * `Subscription<void>` (`unsubscribe` + `onInterrupt`).\n *\n * Type identical to `createThemeProvider()` from\n * `@novasamatech/host-api-wrapper`.\n */\nexport type ThemeProvider = ReturnType<typeof createThemeProvider>;\n\n/**\n * Host theme value. Re-exported from `@novasamatech/host-api-wrapper`.\n *\n * A `{ name, variant }` struct as of v0.8 (previously a flat\n * `\"light\" | \"dark\"` string).\n */\nexport type ThemeMode = NovasamaThemeMode;\n\n/** Light/dark variant of the active theme: `\"Light\" | \"Dark\"`. */\nexport type ThemeVariant = ThemeMode[\"variant\"];\n\n/**\n * Active theme name: `{ tag: \"Default\" }`, or `{ tag: \"Custom\", value }`\n * carrying the custom theme's string id.\n */\nexport type ThemeName = ThemeMode[\"name\"];\n\n/**\n * Get the host theme provider.\n *\n * Returns the theme-subscription handle exported by\n * `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable\n * (running outside a host container or the optional peer dep isn't\n * installed).\n *\n * Implementation note: upstream `@novasamatech/host-api-wrapper` exports only\n * the `createThemeProvider` factory and no `themeProvider` singleton, so\n * this getter constructs a fresh instance on each call (unlike\n * {@link getPreimageManager} or {@link getHostLocalStorage}, which return\n * upstream singletons). The constructed provider is cheap to allocate; it\n * only opens a subscription when `subscribeTheme` is called.\n *\n * @returns The theme provider, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getThemeProvider } from \"@parity/product-sdk-host\";\n *\n * const provider = await getThemeProvider();\n * if (provider) {\n * const sub = provider.subscribeTheme((theme) => {\n * document.documentElement.dataset.theme = theme.variant.toLowerCase();\n * if (theme.name.tag === \"Custom\") loadCustomTheme(theme.name.value);\n * });\n * // sub.unsubscribe() to stop listening\n * }\n * ```\n */\nexport async function getThemeProvider(): Promise<ThemeProvider | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createThemeProvider();\n } catch (err) {\n log.debug(\"getThemeProvider unavailable\", err);\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getThemeProvider returns provider when SDK is available\", async () => {\n const provider = await getThemeProvider();\n expect(provider === null || typeof provider === \"object\").toBe(true);\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Higher-level wrapper for the host's entropy derivation (RFC-0007).\n *\n * `hostApi.deriveEntropy` is reachable via {@link getTruApi}, but consumers\n * have to wrap the value in the versioned envelope (`enumValue(\"v1\", ...)`)\n * and unwrap the neverthrow `ResultAsync` themselves. `deriveEntropy`\n * collapses that to a throw-on-error Promise that matches the shape of\n * {@link requestPermission} and {@link requestResourceAllocation}.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { enumValue, formatHostError, getTruApi } from \"./truapi.js\";\n\nconst log = createLogger(\"host:entropy\");\n\n/**\n * Derive deterministic entropy from a context key (RFC-0007).\n *\n * The host derives entropy from the user's wallet + the provided context\n * key. Calling with the same key on the same wallet yields the same bytes;\n * different keys (or different wallets) yield uncorrelated entropy.\n *\n * @param key - Context key bytes (typically a SCALE-encoded discriminator).\n * @returns The derived entropy bytes.\n * @throws If the host is unavailable or the host-side derivation fails.\n *\n * @example\n * ```ts\n * import { deriveEntropy } from \"@parity/product-sdk-host\";\n *\n * const seed = await deriveEntropy(new TextEncoder().encode(\"my-app:seed-v1\"));\n * ```\n */\nexport async function deriveEntropy(key: Uint8Array): Promise<Uint8Array> {\n const truApi = await getTruApi();\n if (!truApi) {\n throw new Error(\"deriveEntropy: TruAPI unavailable\");\n }\n log.debug(\"deriveEntropy\", { keyLen: key.length });\n\n return await truApi.deriveEntropy(enumValue(\"v1\", key)).match(\n (envelope: { tag: \"v1\"; value: Uint8Array }) => envelope.value,\n (err: unknown) => {\n throw new Error(`deriveEntropy failed: ${formatHostError(err)}`, { cause: err });\n },\n );\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"deriveEntropy throws when TruAPI is unavailable\", async () => {\n const api = await getTruApi();\n if (api === null) {\n await expect(deriveEntropy(new Uint8Array([1, 2, 3]))).rejects.toThrow(\n /TruAPI unavailable/,\n );\n } else {\n expect(typeof deriveEntropy).toBe(\"function\");\n }\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Wrapper for the host's chat surface (`host_chat_*` family).\n *\n * Shipped flat-in-host rather than as `getTruApi().chat.*` (the shape\n * sketched in issue #93) because the upstream JS `hostApi` is itself a\n * flat object - there is no `.chat` accessor to mirror. A flat\n * `getChatManager()` matches the pattern already used by\n * {@link getThemeProvider}, {@link getAccountsProvider}, and\n * {@link getStatementStore}; if a namespaced view is desirable later, it\n * can be layered on top without breaking this surface.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type {\n ChatBotRegistrationResult as NovasamaChatBotRegistrationResult,\n ChatCustomMessageRenderer as NovasamaChatCustomMessageRenderer,\n ChatCustomMessageRendererParams as NovasamaChatCustomMessageRendererParams,\n ChatMessageContent as NovasamaChatMessageContent,\n ChatReceivedAction as NovasamaChatReceivedAction,\n ChatRoom as NovasamaChatRoom,\n ChatRoomRegistrationResult as NovasamaChatRoomRegistrationResult,\n createProductChatManager,\n} from \"@novasamatech/host-api-wrapper\";\n\nconst log = createLogger(\"host:chat\");\n\n/** Chat message payload variants. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatMessageContent = NovasamaChatMessageContent;\n\n/** Action received via {@link ChatManager.subscribeAction}. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatReceivedAction = NovasamaChatReceivedAction;\n\n/** Room metadata delivered to {@link ChatManager.subscribeChatList}. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatRoom = NovasamaChatRoom;\n\n/** Result of registering a chat room (`\"New\" | \"Exists\"`). Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatRoomRegistrationResult = NovasamaChatRoomRegistrationResult;\n\n/** Result of registering a bot (`\"New\" | \"Exists\"`). Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatBotRegistrationResult = NovasamaChatBotRegistrationResult;\n\n/** Renderer callback for custom message types. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatCustomMessageRenderer = NovasamaChatCustomMessageRenderer;\n\n/** Parameters passed to a {@link ChatCustomMessageRenderer}. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type ChatCustomMessageRendererParams<T = Uint8Array> =\n NovasamaChatCustomMessageRendererParams<T>;\n\n/**\n * Chat manager handle. Exposes room/bot registration, message sending,\n * subscription to room list and incoming actions, and custom-renderer\n * registration.\n *\n * Type identical to `createProductChatManager()` from\n * `@novasamatech/host-api-wrapper`.\n */\nexport type ChatManager = ReturnType<typeof createProductChatManager>;\n\n/**\n * Get the host chat manager.\n *\n * Returns the chat manager from `@novasamatech/host-api-wrapper`, or `null` if\n * the package is unavailable (running outside a host container or the\n * optional peer dep isn't installed).\n *\n * @returns The chat manager, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getChatManager } from \"@parity/product-sdk-host\";\n *\n * const chat = await getChatManager();\n * if (chat) {\n * await chat.registerBot({ botId: \"echo\", name: \"Echo Bot\", icon: \"\" });\n * chat.subscribeAction((action) => { ... });\n * }\n * ```\n */\nexport async function getChatManager(): Promise<ChatManager | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.createProductChatManager();\n } catch (err) {\n log.debug(\"getChatManager unavailable\", err);\n return null;\n }\n}\n\n/**\n * Dispatch helper that composes multiple custom-message renderers into a\n * single {@link ChatCustomMessageRenderer} keyed by `messageType`.\n *\n * Mirrors `matchChatCustomRenderers` from `@novasamatech/host-api-wrapper`\n * inline (the upstream implementation is pure dispatch logic with no\n * transport / runtime dependency on Novasama), so callers get the same\n * sync signature instead of an async-with-null wrapper.\n *\n * @param map - Object mapping `messageType` strings to renderers.\n * @returns A composed renderer that dispatches to the entry matching\n * `params.messageType`, or throws if no renderer is registered.\n */\nexport function matchChatCustomRenderers(\n map: Record<string, ChatCustomMessageRenderer>,\n): ChatCustomMessageRenderer {\n return (params, render) => {\n const renderer = map[params.messageType];\n if (!renderer) {\n throw new Error(`Renderer for message type ${params.messageType} is not defined`);\n }\n return renderer(params, render);\n };\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getChatManager returns manager when SDK is available\", async () => {\n const chat = await getChatManager();\n expect(chat === null || typeof chat === \"object\").toBe(true);\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Wrapper for the host's payment manager (RFC-0006).\n *\n * Shipped flat-in-host rather than as `getTruApi().payment.*` because the\n * upstream JS `hostApi` is itself a flat object - there is no `.payment`\n * accessor to mirror. A flat `getPaymentManager()` matches the singleton\n * pattern already used by {@link getPreimageManager},\n * {@link getHostLocalStorage}, and {@link getAccountsProvider}.\n *\n * Returns the shared `paymentManager` singleton from\n * `@novasamatech/host-api-wrapper` (not a fresh `createPaymentManager()`\n * instance) so callers share one wrapper + hostApi closure across the app.\n *\n * Distinct from the CoinPayment / merchant-payments surface tracked under\n * `@parity/product-sdk-merchant-payments` (RFC-0017). RFC-0006 is the\n * user-initiated balance / top-up / payment-request flow; RFC-0017 is the\n * merchant-initiated checkout flow.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type {\n PaymentBalance as NovasamaPaymentBalance,\n PaymentStatus as NovasamaPaymentStatus,\n TopUpSource as NovasamaTopUpSource,\n paymentManager,\n} from \"@novasamatech/host-api-wrapper\";\n\nconst log = createLogger(\"host:payments\");\n\n/** Available balance for the user's payment account. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type PaymentBalance = NovasamaPaymentBalance;\n\n/** Status of an in-flight payment request. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type PaymentStatus = NovasamaPaymentStatus;\n\n/** Source for {@link PaymentManager.topUp}. Re-exported from `@novasamatech/host-api-wrapper`. */\nexport type TopUpSource = NovasamaTopUpSource;\n\n/**\n * Payment manager handle. Exposes balance subscription, top-up, payment\n * requests, and payment status subscription.\n *\n * Type identical to `paymentManager` from `@novasamatech/host-api-wrapper`.\n */\nexport type PaymentManager = typeof paymentManager;\n\n/**\n * Get the host payment manager.\n *\n * Returns the shared `paymentManager` singleton from\n * `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable\n * (running outside a host container or the optional peer dep isn't\n * installed).\n *\n * @returns The payment manager, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getPaymentManager } from \"@parity/product-sdk-host\";\n *\n * const payments = await getPaymentManager();\n * if (payments) {\n * const sub = payments.subscribeBalance((b) => { ... });\n * await payments.topUp(1_000_000n, { type: \"productAccount\", derivationIndex: 0 });\n * const destination = new Uint8Array(32);\n * const { id } = await payments.requestPayment(500n, destination);\n * sub.unsubscribe();\n * }\n * ```\n */\nexport async function getPaymentManager(): Promise<PaymentManager | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.paymentManager;\n } catch (err) {\n log.debug(\"getPaymentManager unavailable\", err);\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getPaymentManager returns manager with full RFC-0006 surface when SDK is available\", async () => {\n const payments = await getPaymentManager();\n if (payments === null) {\n // Acceptable: SDK couldn't load (e.g. peer dep missing in some envs).\n return;\n }\n expect(typeof payments.subscribeBalance).toBe(\"function\");\n expect(typeof payments.topUp).toBe(\"function\");\n expect(typeof payments.requestPayment).toBe(\"function\");\n expect(typeof payments.subscribePaymentStatus).toBe(\"function\");\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Wrapper for the host's scheduled push-notification surface.\n *\n * Shipped flat-in-host rather than as `getTruApi().notification.*` because\n * the upstream JS `hostApi` is itself a flat object - there is no\n * `.notification` accessor to mirror. A flat `getNotificationManager()`\n * matches the singleton pattern already used by {@link getPaymentManager},\n * {@link getPreimageManager}, and {@link getHostLocalStorage}.\n *\n * Returns the shared `notificationManager` singleton from\n * `@novasamatech/host-api-wrapper` (not a fresh `createNotificationManager()`\n * instance) so callers share one wrapper + hostApi closure across the app.\n *\n * {@link PushNotificationError} is re-exported from `@novasamatech/host-api`\n * so consumers can branch on `err instanceof\n * PushNotificationError.ScheduleLimitReached` (the host's pending-notification\n * cap) without importing the novasama packages directly.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type { notificationManager } from \"@novasamatech/host-api-wrapper\";\n\nconst log = createLogger(\"host:notifications\");\n\n/**\n * Error variants the host raises when scheduling a push notification.\n *\n * A SCALE codec (with a `[Symbol.hasInstance]`), not a plain `Error`\n * subclass: branch with `err instanceof\n * PushNotificationError.ScheduleLimitReached` to detect the host's\n * platform-wide pending-notification cap, or `.Unknown` for everything\n * else. Re-exported from `@novasamatech/host-api` so consumers can\n * `instanceof`-branch without a direct novasama dependency.\n */\nexport { PushNotificationError } from \"@novasamatech/host-api\";\n\n/**\n * Host notification manager handle. Exposes `push(input)` (resolves to a\n * {@link NotificationId}) and `cancel(id)`.\n *\n * Type identical to `notificationManager` from\n * `@novasamatech/host-api-wrapper`.\n */\nexport type NotificationManager = typeof notificationManager;\n\n/**\n * Host-assigned id for a scheduled notification — pass to\n * {@link NotificationManager.cancel}. Derived from the manager's `push`\n * return type so codec changes surface here as compile errors.\n */\nexport type NotificationId = Awaited<ReturnType<NotificationManager[\"push\"]>>;\n\n/**\n * Push payload: `text`, an optional `deeplink`, and an optional\n * `scheduledAt` (omit for immediate delivery). Derived from the manager's\n * `push` parameter so the shape stays in lockstep with upstream.\n */\nexport type PushNotificationInput = Parameters<NotificationManager[\"push\"]>[0];\n\n/**\n * Get the host notification manager.\n *\n * Returns the shared `notificationManager` singleton from\n * `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable\n * (running outside a host container or the optional peer dep isn't\n * installed).\n *\n * @returns The notification manager, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getNotificationManager, PushNotificationError } from \"@parity/product-sdk-host\";\n *\n * const notifications = await getNotificationManager();\n * if (notifications) {\n * try {\n * const id = await notifications.push({\n * text: \"Doors open in 1h\",\n * scheduledAt: someUnixMs,\n * });\n * // later: await notifications.cancel(id);\n * } catch (err) {\n * if (err instanceof PushNotificationError.ScheduleLimitReached) {\n * // host hit its pending-notification cap — surface to the user\n * }\n * }\n * }\n * ```\n */\nexport async function getNotificationManager(): Promise<NotificationManager | null> {\n try {\n const sdk = await import(\"@novasamatech/host-api-wrapper\");\n return sdk.notificationManager;\n } catch (err) {\n log.debug(\"getNotificationManager unavailable\", err);\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getNotificationManager returns manager with push/cancel when SDK is available\", async () => {\n const notifications = await getNotificationManager();\n if (notifications === null) {\n // Acceptable: SDK couldn't load (e.g. peer dep missing in some envs).\n return;\n }\n expect(typeof notifications.push).toBe(\"function\");\n expect(typeof notifications.cancel).toBe(\"function\");\n });\n\n test(\"PushNotificationError is re-exported with its ScheduleLimitReached variant\", async () => {\n const { PushNotificationError } = await import(\"./notifications.js\");\n expect(PushNotificationError).toBeDefined();\n expect(PushNotificationError.ScheduleLimitReached).toBeDefined();\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parity/product-sdk-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Host container detection and storage access for Polkadot Desktop and Mobile environments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,24 +18,13 @@
|
|
|
18
18
|
"src"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@novasamatech/host-api": "^0.8.7",
|
|
22
|
+
"@novasamatech/host-api-wrapper": "^0.8.7",
|
|
21
23
|
"polkadot-api": "^2.1.5",
|
|
22
24
|
"@parity/product-sdk-logger": "0.1.1"
|
|
23
25
|
},
|
|
24
|
-
"peerDependencies": {
|
|
25
|
-
"@novasamatech/host-api-wrapper": ">=0.8.0",
|
|
26
|
-
"@novasamatech/host-api": ">=0.8.0"
|
|
27
|
-
},
|
|
28
|
-
"peerDependenciesMeta": {
|
|
29
|
-
"@novasamatech/host-api-wrapper": {
|
|
30
|
-
"optional": true
|
|
31
|
-
},
|
|
32
|
-
"@novasamatech/host-api": {
|
|
33
|
-
"optional": true
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
26
|
"devDependencies": {
|
|
37
|
-
"
|
|
38
|
-
"@novasamatech/host-api": "^0.8.6",
|
|
27
|
+
"tsup": "^8.5.1",
|
|
39
28
|
"typescript": "^5.7.0",
|
|
40
29
|
"vitest": "^3.0.0"
|
|
41
30
|
},
|
package/src/index.ts
CHANGED
|
@@ -95,3 +95,11 @@ export type {
|
|
|
95
95
|
// Payments (RFC-0006)
|
|
96
96
|
export { getPaymentManager } from "./payments.js";
|
|
97
97
|
export type { PaymentManager, PaymentBalance, PaymentStatus, TopUpSource } from "./payments.js";
|
|
98
|
+
|
|
99
|
+
// Notifications
|
|
100
|
+
export { getNotificationManager, PushNotificationError } from "./notifications.js";
|
|
101
|
+
export type {
|
|
102
|
+
NotificationManager,
|
|
103
|
+
NotificationId,
|
|
104
|
+
PushNotificationInput,
|
|
105
|
+
} from "./notifications.js";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper for the host's scheduled push-notification surface.
|
|
5
|
+
*
|
|
6
|
+
* Shipped flat-in-host rather than as `getTruApi().notification.*` because
|
|
7
|
+
* the upstream JS `hostApi` is itself a flat object - there is no
|
|
8
|
+
* `.notification` accessor to mirror. A flat `getNotificationManager()`
|
|
9
|
+
* matches the singleton pattern already used by {@link getPaymentManager},
|
|
10
|
+
* {@link getPreimageManager}, and {@link getHostLocalStorage}.
|
|
11
|
+
*
|
|
12
|
+
* Returns the shared `notificationManager` singleton from
|
|
13
|
+
* `@novasamatech/host-api-wrapper` (not a fresh `createNotificationManager()`
|
|
14
|
+
* instance) so callers share one wrapper + hostApi closure across the app.
|
|
15
|
+
*
|
|
16
|
+
* {@link PushNotificationError} is re-exported from `@novasamatech/host-api`
|
|
17
|
+
* so consumers can branch on `err instanceof
|
|
18
|
+
* PushNotificationError.ScheduleLimitReached` (the host's pending-notification
|
|
19
|
+
* cap) without importing the novasama packages directly.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
25
|
+
|
|
26
|
+
import type { notificationManager } from "@novasamatech/host-api-wrapper";
|
|
27
|
+
|
|
28
|
+
const log = createLogger("host:notifications");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Error variants the host raises when scheduling a push notification.
|
|
32
|
+
*
|
|
33
|
+
* A SCALE codec (with a `[Symbol.hasInstance]`), not a plain `Error`
|
|
34
|
+
* subclass: branch with `err instanceof
|
|
35
|
+
* PushNotificationError.ScheduleLimitReached` to detect the host's
|
|
36
|
+
* platform-wide pending-notification cap, or `.Unknown` for everything
|
|
37
|
+
* else. Re-exported from `@novasamatech/host-api` so consumers can
|
|
38
|
+
* `instanceof`-branch without a direct novasama dependency.
|
|
39
|
+
*/
|
|
40
|
+
export { PushNotificationError } from "@novasamatech/host-api";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Host notification manager handle. Exposes `push(input)` (resolves to a
|
|
44
|
+
* {@link NotificationId}) and `cancel(id)`.
|
|
45
|
+
*
|
|
46
|
+
* Type identical to `notificationManager` from
|
|
47
|
+
* `@novasamatech/host-api-wrapper`.
|
|
48
|
+
*/
|
|
49
|
+
export type NotificationManager = typeof notificationManager;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Host-assigned id for a scheduled notification — pass to
|
|
53
|
+
* {@link NotificationManager.cancel}. Derived from the manager's `push`
|
|
54
|
+
* return type so codec changes surface here as compile errors.
|
|
55
|
+
*/
|
|
56
|
+
export type NotificationId = Awaited<ReturnType<NotificationManager["push"]>>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Push payload: `text`, an optional `deeplink`, and an optional
|
|
60
|
+
* `scheduledAt` (omit for immediate delivery). Derived from the manager's
|
|
61
|
+
* `push` parameter so the shape stays in lockstep with upstream.
|
|
62
|
+
*/
|
|
63
|
+
export type PushNotificationInput = Parameters<NotificationManager["push"]>[0];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the host notification manager.
|
|
67
|
+
*
|
|
68
|
+
* Returns the shared `notificationManager` singleton from
|
|
69
|
+
* `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable
|
|
70
|
+
* (running outside a host container or the optional peer dep isn't
|
|
71
|
+
* installed).
|
|
72
|
+
*
|
|
73
|
+
* @returns The notification manager, or `null` if unavailable.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { getNotificationManager, PushNotificationError } from "@parity/product-sdk-host";
|
|
78
|
+
*
|
|
79
|
+
* const notifications = await getNotificationManager();
|
|
80
|
+
* if (notifications) {
|
|
81
|
+
* try {
|
|
82
|
+
* const id = await notifications.push({
|
|
83
|
+
* text: "Doors open in 1h",
|
|
84
|
+
* scheduledAt: someUnixMs,
|
|
85
|
+
* });
|
|
86
|
+
* // later: await notifications.cancel(id);
|
|
87
|
+
* } catch (err) {
|
|
88
|
+
* if (err instanceof PushNotificationError.ScheduleLimitReached) {
|
|
89
|
+
* // host hit its pending-notification cap — surface to the user
|
|
90
|
+
* }
|
|
91
|
+
* }
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export async function getNotificationManager(): Promise<NotificationManager | null> {
|
|
96
|
+
try {
|
|
97
|
+
const sdk = await import("@novasamatech/host-api-wrapper");
|
|
98
|
+
return sdk.notificationManager;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
log.debug("getNotificationManager unavailable", err);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (import.meta.vitest) {
|
|
106
|
+
const { test, expect } = import.meta.vitest;
|
|
107
|
+
|
|
108
|
+
test("getNotificationManager returns manager with push/cancel when SDK is available", async () => {
|
|
109
|
+
const notifications = await getNotificationManager();
|
|
110
|
+
if (notifications === null) {
|
|
111
|
+
// Acceptable: SDK couldn't load (e.g. peer dep missing in some envs).
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
expect(typeof notifications.push).toBe("function");
|
|
115
|
+
expect(typeof notifications.cancel).toBe("function");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("PushNotificationError is re-exported with its ScheduleLimitReached variant", async () => {
|
|
119
|
+
const { PushNotificationError } = await import("./notifications.js");
|
|
120
|
+
expect(PushNotificationError).toBeDefined();
|
|
121
|
+
expect(PushNotificationError.ScheduleLimitReached).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
}
|