@parity/product-sdk-host 0.2.2 → 0.4.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 CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as polkadot_api from 'polkadot-api';
2
2
  import { JsonRpcProvider } from 'polkadot-api';
3
+ import { CodecType, AllocatableResource as AllocatableResource$1, AllocationOutcome as AllocationOutcome$1, RemotePermission as RemotePermission$1, Statement as Statement$1 } from '@novasamatech/host-api';
3
4
  export { HexString, assertEnumVariant, enumValue, fromHex, isEnumVariant, resultErr, resultOk, toHex, unwrapResultOrThrow } from '@novasamatech/host-api';
4
5
 
5
6
  /**
@@ -276,6 +277,108 @@ interface PreimageManager {
276
277
  * @returns The accounts provider, or `null` if unavailable.
277
278
  */
278
279
  declare function getAccountsProvider(): Promise<AccountsProvider | null>;
280
+ /**
281
+ * Resource types requestable via {@link requestResourceAllocation}.
282
+ * Derived from the upstream codec so variant renames surface as compile
283
+ * errors, not runtime failures.
284
+ */
285
+ type AllocatableResource = CodecType<typeof AllocatableResource$1>;
286
+ /** Tag-only view of {@link AllocatableResource} for places that just need the variant name. */
287
+ type AllocatableResourceTag = AllocatableResource["tag"];
288
+ /**
289
+ * Per-resource outcome from {@link requestResourceAllocation}.
290
+ * The host strips secret payloads from `Allocated` before returning, so
291
+ * `value` is always `undefined` on the product side.
292
+ */
293
+ type AllocationOutcome = CodecType<typeof AllocationOutcome$1>;
294
+ /** Tag-only view of {@link AllocationOutcome} (`"Allocated" | "Rejected" | "NotAvailable"`). */
295
+ type AllocationOutcomeTag = AllocationOutcome["tag"];
296
+ /**
297
+ * Remote permission the dapp can ask the host to grant via
298
+ * {@link requestPermission}.
299
+ *
300
+ * Derived from the upstream codec so variant renames surface as compile
301
+ * errors, not runtime failures.
302
+ */
303
+ type RemotePermission = CodecType<typeof RemotePermission$1>;
304
+ /** Tag-only view of {@link RemotePermission}. */
305
+ type RemotePermissionTag = RemotePermission["tag"];
306
+ /**
307
+ * Request the host to pre-allocate one or more resource allowances.
308
+ *
309
+ * The host prompts the user once; subsequent operations covered by the
310
+ * granted allowance don't re-prompt.
311
+ *
312
+ * @param resources - Resources to request.
313
+ * @returns Per-resource outcomes in the same order as `resources`.
314
+ * @throws If the host is unavailable or the request fails.
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * const outcomes = await requestResourceAllocation([
319
+ * { tag: "BulletInAllowance", value: undefined },
320
+ * ]);
321
+ * if (outcomes[0].tag === "Allocated") { ... }
322
+ * ```
323
+ */
324
+ declare function requestResourceAllocation(resources: AllocatableResource[]): Promise<AllocationOutcome[]>;
325
+ /**
326
+ * A Statement payload destined for the Statement Store. Matches the
327
+ * `pallet-statement` Statement structure.
328
+ *
329
+ * The optional `proof` field is the same {@link StatementProof} shape that
330
+ * {@link createProofAuthorized} returns: pass `undefined` here, call
331
+ * `createProofAuthorized` to obtain the proof, then attach it before
332
+ * submitting via `HostStatementStore.submit`. The `OnChain` variant of
333
+ * `StatementProof` is a chain-attestation reference; the `Sr25519` /
334
+ * `Ed25519` / `Ecdsa` variants are signing proofs.
335
+ *
336
+ * Derived from the upstream codec so structural changes surface as compile
337
+ * errors here, not runtime decode failures.
338
+ */
339
+ type Statement = CodecType<typeof Statement$1>;
340
+ /**
341
+ * Have the host sign a Statement using an allowance-bearing account it
342
+ * picks internally — RFC-10 §"Statement Store allowance".
343
+ *
344
+ * The product passes only the Statement payload; the host chooses the
345
+ * `//allowance//statement-store//{productId}` account that holds SSS
346
+ * allowance and signs with it. Allowance is provisioned implicitly on
347
+ * first use if the host hasn't already pre-allocated via
348
+ * {@link requestResourceAllocation}; products never see the signing
349
+ * account or its key material.
350
+ *
351
+ * Pairs with {@link getStatementStore}'s `submit`: call this to obtain
352
+ * a proof, attach it to the Statement, and submit the result.
353
+ *
354
+ * @param statement - The Statement to be signed.
355
+ * @returns The proof to attach before submitting.
356
+ * @throws If the host is unavailable or the host-side signing fails.
357
+ *
358
+ * @example
359
+ * ```ts
360
+ * import { createProofAuthorized, getStatementStore } from "@parity/product-sdk-host";
361
+ *
362
+ * const statement = {
363
+ * proof: undefined,
364
+ * decryptionKey: undefined,
365
+ * expiry: undefined,
366
+ * channel: undefined,
367
+ * topics: [],
368
+ * data: payload,
369
+ * };
370
+ * const proof = await createProofAuthorized(statement);
371
+ * const store = await getStatementStore();
372
+ * await store?.submit({ ...statement, proof });
373
+ * ```
374
+ *
375
+ * @remarks
376
+ * RFC-10 introduces this as a new, strictly additive TruAPI call. The
377
+ * pre-existing `HostStatementStore.createProof(accountId, statement)`
378
+ * surface stays available for products that own a non-allowance signing
379
+ * account; this wrapper is the sponsored-submission path.
380
+ */
381
+ declare function createProofAuthorized(statement: Statement): Promise<StatementProof>;
279
382
  /**
280
383
  * One of the user's existing wallet accounts, surfaced through the host and
281
384
  * identified by its public key and an optional name. Contrast with
@@ -397,4 +500,37 @@ interface AccountsProvider {
397
500
  } | (() => void);
398
501
  }
399
502
 
400
- export { type AccountsProvider, BULLETIN_RPCS, type ContextualAlias, DEFAULT_BULLETIN_ENDPOINT, type HostAccount, type HostLocalStorage, type HostStatementStore, type HostSubscription, type PreimageManager, type ProductAccount, type ResultAsync, type StatementProof, type StatementTopicFilter, type StatementsPage, type TruApi, getAccountsProvider, getHostLocalStorage, getHostProvider, getPreimageManager, getStatementStore, getTruApi, isInsideContainer, isInsideContainerSync };
503
+ /**
504
+ * Higher-level wrapper for the host's single-permission flow.
505
+ *
506
+ * `hostApi.permission` takes a versioned envelope (`enumValue("v1", ...)`)
507
+ * and returns a neverthrow `ResultAsync` of an unwrapped versioned response.
508
+ * Consumers rebuild that wrap/unwrap dance every time. `requestPermission`
509
+ * collapses it to a one-liner that matches the shape of
510
+ * {@link requestResourceAllocation} (throws on error, returns the unwrapped
511
+ * payload on success).
512
+ *
513
+ * @module
514
+ */
515
+
516
+ /**
517
+ * Request a single remote permission from the host.
518
+ *
519
+ * Builds the `v1` envelope, calls `hostApi.permission`, unwraps the response,
520
+ * and returns the host's boolean granted/denied outcome.
521
+ *
522
+ * @param permission - The remote permission to request.
523
+ * @returns `true` if the host granted the permission, `false` if denied.
524
+ * @throws If the host is unavailable or the request fails.
525
+ *
526
+ * @example
527
+ * ```ts
528
+ * const granted = await requestPermission({ tag: "ChainSubmit", value: undefined });
529
+ * if (!granted) {
530
+ * tellUserToReconnect();
531
+ * }
532
+ * ```
533
+ */
534
+ declare function requestPermission(permission: RemotePermission): Promise<boolean>;
535
+
536
+ export { type AccountsProvider, type AllocatableResource, type AllocatableResourceTag, type AllocationOutcome, type AllocationOutcomeTag, BULLETIN_RPCS, type ContextualAlias, DEFAULT_BULLETIN_ENDPOINT, type HostAccount, type HostLocalStorage, type HostStatementStore, type HostSubscription, type PreimageManager, type ProductAccount, type RemotePermission, type RemotePermissionTag, type ResultAsync, type Statement, type StatementProof, type StatementTopicFilter, type StatementsPage, type TruApi, createProofAuthorized, getAccountsProvider, getHostLocalStorage, getHostProvider, getPreimageManager, getStatementStore, getTruApi, isInsideContainer, isInsideContainerSync, requestPermission, requestResourceAllocation };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from '@parity/product-sdk-logger';
2
+ import { enumValue } from '@novasamatech/host-api';
2
3
  export { assertEnumVariant, enumValue, fromHex, isEnumVariant, resultErr, resultOk, toHex, unwrapResultOrThrow } from '@novasamatech/host-api';
3
4
 
4
5
  // src/container.ts
@@ -58,6 +59,15 @@ var BULLETIN_RPCS = {
58
59
  };
59
60
  var DEFAULT_BULLETIN_ENDPOINT = BULLETIN_RPCS.paseo[0];
60
61
  var log = createLogger("host");
62
+ function formatError(err) {
63
+ if (err instanceof Error) return err.message;
64
+ if (typeof err === "string") return err;
65
+ try {
66
+ return JSON.stringify(err);
67
+ } catch {
68
+ return String(err);
69
+ }
70
+ }
61
71
  var cachedTruApi = null;
62
72
  async function getTruApi() {
63
73
  if (cachedTruApi) return cachedTruApi;
@@ -87,7 +97,55 @@ async function getAccountsProvider() {
87
97
  return null;
88
98
  }
89
99
  }
100
+ async function requestResourceAllocation(resources) {
101
+ const truApi = await getTruApi();
102
+ if (!truApi) {
103
+ throw new Error("requestResourceAllocation: TruAPI unavailable");
104
+ }
105
+ log.debug("requestResourceAllocation", { resources: resources.map((r) => r.tag) });
106
+ return await truApi.requestResourceAllocation(enumValue("v1", resources)).match(
107
+ (envelope) => envelope.value,
108
+ (err) => {
109
+ throw new Error(`requestResourceAllocation failed: ${formatError(err)}`, {
110
+ cause: err
111
+ });
112
+ }
113
+ );
114
+ }
115
+ async function createProofAuthorized(statement) {
116
+ const truApi = await getTruApi();
117
+ if (!truApi) {
118
+ throw new Error("createProofAuthorized: TruAPI unavailable");
119
+ }
120
+ log.debug("createProofAuthorized", {
121
+ topics: statement.topics.length,
122
+ dataLen: statement.data?.length ?? 0
123
+ });
124
+ return await truApi.statementStoreCreateProofAuthorized(enumValue("v1", statement)).match(
125
+ (envelope) => envelope.value,
126
+ (err) => {
127
+ throw new Error(`createProofAuthorized failed: ${formatError(err)}`, { cause: err });
128
+ }
129
+ );
130
+ }
131
+ var log2 = createLogger("host:permissions");
132
+ async function requestPermission(permission) {
133
+ const truApi = await getTruApi();
134
+ if (!truApi) {
135
+ throw new Error("requestPermission: TruAPI unavailable");
136
+ }
137
+ log2.debug("requestPermission", { tag: permission.tag });
138
+ return await truApi.permission(enumValue("v1", permission)).match(
139
+ (envelope) => envelope.value,
140
+ (err) => {
141
+ throw new Error(
142
+ `requestPermission failed: ${err instanceof Error ? err.message : String(err)}`,
143
+ { cause: err }
144
+ );
145
+ }
146
+ );
147
+ }
90
148
 
91
- export { BULLETIN_RPCS, DEFAULT_BULLETIN_ENDPOINT, getAccountsProvider, getHostLocalStorage, getHostProvider, getPreimageManager, getStatementStore, getTruApi, isInsideContainer, isInsideContainerSync };
149
+ export { BULLETIN_RPCS, DEFAULT_BULLETIN_ENDPOINT, createProofAuthorized, getAccountsProvider, getHostLocalStorage, getHostProvider, getPreimageManager, getStatementStore, getTruApi, isInsideContainer, isInsideContainerSync, requestPermission, requestResourceAllocation };
92
150
  //# sourceMappingURL=index.js.map
93
151
  //# 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"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { JsonRpcProvider } from \"polkadot-api\";\n\nimport type { HostLocalStorage, HostStatementStore } from \"./types.js\";\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/product-sdk\");\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/product-sdk\");\n return sdk.hostLocalStorage as HostLocalStorage;\n } catch {\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/product-sdk` is unavailable.\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 */\nexport async function getHostProvider(genesisHash: `0x${string}`): Promise<JsonRpcProvider | null> {\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n return sdk.createPapiProvider(genesisHash);\n } catch {\n return null;\n }\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/product-sdk` 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/product-sdk\");\n return sdk.createStatementStore() as HostStatementStore;\n } catch {\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, vi } = import.meta.vitest;\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(\"getHostProvider returns null when product-sdk unavailable\", async () => {\n const result = await getHostProvider(\"0xabc\");\n expect(result).toBeNull();\n });\n\n test(\"getStatementStore returns null when product-sdk unavailable\", async () => {\n const result = await getStatementStore();\n expect(result).toBeNull();\n });\n}\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 `previewnet`\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 previewnet: [\"wss://previewnet.substrate.dev/bulletin\"],\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 previewnet endpoint\", () => {\n expect(BULLETIN_RPCS.previewnet.length).toBeGreaterThan(0);\n expect(BULLETIN_RPCS.previewnet[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","/**\n * TruAPI - the protocol for communicating between apps and the Polkadot host container.\n *\n * This module centralizes access to @novasamatech/product-sdk 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\nconst log = createLogger(\"host\");\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// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type TruApi = any;\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/product-sdk` 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/product-sdk\");\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/product-sdk\");\n return sdk.preimageManager;\n } catch {\n return null;\n }\n}\n\n/**\n * Preimage manager interface for bulletin chain operations.\n */\nexport interface PreimageManager {\n /**\n * Submit a preimage to the bulletin chain.\n * @param data - The data to submit.\n * @returns The preimage key (hex string).\n */\n submit(data: Uint8Array): Promise<string>;\n\n /**\n * Look up a preimage by key.\n * @param key - The preimage key (hex string).\n * @param callback - Called with the data when found, or null if not yet available.\n * @returns Subscription handle with unsubscribe method.\n */\n lookup(\n key: string,\n callback: (preimage: Uint8Array | null) => void,\n ): { unsubscribe: () => void; onInterrupt: (cb: () => void) => () => void };\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/product-sdk\");\n return sdk.createAccountsProvider() as unknown as AccountsProvider;\n } catch {\n return null;\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 interface from @novasamatech/product-sdk.\n *\n * Provides methods for accessing host wallet accounts, product accounts,\n * and Ring VRF operations.\n */\nexport interface AccountsProvider {\n /**\n * Get legacy accounts (user's external wallets connected to the host).\n *\n * Renamed from `getNonProductAccounts` in @novasamatech/product-sdk 0.7.\n *\n * @returns ResultAsync resolving to array of accounts.\n */\n getLegacyAccounts: () => ResultAsync<HostAccount[], unknown>;\n\n /**\n * Get a signer for a legacy account.\n *\n * Renamed from `getNonProductAccountSigner` in @novasamatech/product-sdk 0.7.\n *\n * @param account - The product account (used for public key lookup).\n * @returns A PolkadotSigner for signing transactions.\n */\n getLegacyAccountSigner: (account: ProductAccount) => import(\"polkadot-api\").PolkadotSigner;\n\n /**\n * Get an app-scoped product account from the host.\n *\n * Product accounts are derived by the host wallet for each app, identified\n * by `dotNsIdentifier` (e.g., \"mark3t.dot\"). The user controls these accounts\n * but they are scoped to the requesting app.\n *\n * @param dotNsIdentifier - App identifier (e.g., \"mark3t.dot\").\n * @param derivationIndex - Derivation index within the app scope. Default: 0\n * @returns ResultAsync resolving to the account.\n */\n getProductAccount: (\n dotNsIdentifier: string,\n derivationIndex?: number,\n ) => ResultAsync<HostAccount, unknown>;\n\n /**\n * Get a signer for a product account.\n *\n * @param account - The product account.\n * @returns A PolkadotSigner for signing transactions.\n */\n getProductAccountSigner: (account: ProductAccount) => import(\"polkadot-api\").PolkadotSigner;\n\n /**\n * Get a contextual alias for a product account via Ring VRF.\n *\n * Aliases prove account membership in a ring without revealing which\n * account produced the alias.\n *\n * @param dotNsIdentifier - App identifier.\n * @param derivationIndex - Derivation index. Default: 0\n * @returns ResultAsync resolving to the contextual alias.\n */\n getProductAccountAlias: (\n dotNsIdentifier: string,\n derivationIndex?: number,\n ) => ResultAsync<ContextualAlias, unknown>;\n\n /**\n * Create a Ring VRF proof for anonymous operations.\n *\n * Proves that the signer is a member of the ring at the given location\n * without revealing which member.\n *\n * @param dotNsIdentifier - App identifier.\n * @param derivationIndex - Derivation index.\n * @param location - Ring location on-chain.\n * @param message - Message to sign.\n * @returns ResultAsync resolving to the proof bytes.\n */\n createRingVRFProof: (\n dotNsIdentifier: string,\n derivationIndex: number,\n location: unknown,\n message: Uint8Array,\n ) => ResultAsync<Uint8Array, unknown>;\n\n /**\n * Subscribe to account connection status changes.\n *\n * @param callback - Called with status string (\"connected\" | \"disconnected\").\n * @returns Unsubscribe handle.\n */\n subscribeAccountConnectionStatus: (\n callback: (status: string) => void,\n ) => { unsubscribe: () => void } | (() => void);\n}\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(\"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"]}
1
+ {"version":3,"sources":["../src/container.ts","../src/chains.ts","../src/truapi.ts","../src/permissions.ts"],"names":["log","createLogger","enumValue"],"mappings":"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","file":"index.js","sourcesContent":["import type { JsonRpcProvider } from \"polkadot-api\";\n\nimport type { HostLocalStorage, HostStatementStore } from \"./types.js\";\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/product-sdk\");\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/product-sdk\");\n return sdk.hostLocalStorage as HostLocalStorage;\n } catch {\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/product-sdk` is unavailable.\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 */\nexport async function getHostProvider(genesisHash: `0x${string}`): Promise<JsonRpcProvider | null> {\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n return sdk.createPapiProvider(genesisHash);\n } catch {\n return null;\n }\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/product-sdk` 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/product-sdk\");\n return sdk.createStatementStore() as HostStatementStore;\n } catch {\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, vi } = import.meta.vitest;\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(\"getHostProvider returns null when product-sdk unavailable\", async () => {\n const result = await getHostProvider(\"0xabc\");\n expect(result).toBeNull();\n });\n\n test(\"getStatementStore returns null when product-sdk unavailable\", async () => {\n const result = await getStatementStore();\n expect(result).toBeNull();\n });\n}\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 `previewnet`\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 previewnet: [\"wss://previewnet.substrate.dev/bulletin\"],\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 previewnet endpoint\", () => {\n expect(BULLETIN_RPCS.previewnet.length).toBeGreaterThan(0);\n expect(BULLETIN_RPCS.previewnet[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","/**\n * TruAPI - the protocol for communicating between apps and the Polkadot host container.\n *\n * This module centralizes access to @novasamatech/product-sdk 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 Statement as StatementCodec,\n} from \"@novasamatech/host-api\";\n\nimport type { StatementProof } from \"./types.js\";\n\nconst log = createLogger(\"host\");\n\n/**\n * Extract a human-readable message from an unknown error. `JSON.stringify`\n * on `Error` returns `\"{}\"` because `message` and `stack` are non-enumerable\n * — without this helper, wire failures surface as `\"... failed: {}\"` with\n * zero diagnostic context.\n */\nfunction formatError(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === \"string\") return err;\n try {\n return JSON.stringify(err);\n } catch {\n return String(err);\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// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type TruApi = any;\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/product-sdk` 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/product-sdk\");\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/product-sdk\");\n return sdk.preimageManager;\n } catch {\n return null;\n }\n}\n\n/**\n * Preimage manager interface for bulletin chain operations.\n */\nexport interface PreimageManager {\n /**\n * Submit a preimage to the bulletin chain.\n * @param data - The data to submit.\n * @returns The preimage key (hex string).\n */\n submit(data: Uint8Array): Promise<string>;\n\n /**\n * Look up a preimage by key.\n * @param key - The preimage key (hex string).\n * @param callback - Called with the data when found, or null if not yet available.\n * @returns Subscription handle with unsubscribe method.\n */\n lookup(\n key: string,\n callback: (preimage: Uint8Array | null) => void,\n ): { unsubscribe: () => void; onInterrupt: (cb: () => void) => () => void };\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/product-sdk\");\n return sdk.createAccountsProvider() as unknown as AccountsProvider;\n } catch {\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: ${formatError(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 * A Statement payload destined for the Statement Store. Matches the\n * `pallet-statement` Statement structure.\n *\n * The optional `proof` field is the same {@link StatementProof} shape that\n * {@link createProofAuthorized} returns: pass `undefined` here, call\n * `createProofAuthorized` to obtain the proof, then attach it before\n * submitting via `HostStatementStore.submit`. The `OnChain` variant of\n * `StatementProof` is a chain-attestation reference; the `Sr25519` /\n * `Ed25519` / `Ecdsa` variants are signing proofs.\n *\n * Derived from the upstream codec so structural changes surface as compile\n * errors here, not runtime decode failures.\n */\nexport type Statement = CodecType<typeof StatementCodec>;\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: ${formatError(err)}`, { cause: err });\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 interface from @novasamatech/product-sdk.\n *\n * Provides methods for accessing host wallet accounts, product accounts,\n * and Ring VRF operations.\n */\nexport interface AccountsProvider {\n /**\n * Get legacy accounts (user's external wallets connected to the host).\n *\n * Renamed from `getNonProductAccounts` in @novasamatech/product-sdk 0.7.\n *\n * @returns ResultAsync resolving to array of accounts.\n */\n getLegacyAccounts: () => ResultAsync<HostAccount[], unknown>;\n\n /**\n * Get a signer for a legacy account.\n *\n * Renamed from `getNonProductAccountSigner` in @novasamatech/product-sdk 0.7.\n *\n * @param account - The product account (used for public key lookup).\n * @returns A PolkadotSigner for signing transactions.\n */\n getLegacyAccountSigner: (account: ProductAccount) => import(\"polkadot-api\").PolkadotSigner;\n\n /**\n * Get an app-scoped product account from the host.\n *\n * Product accounts are derived by the host wallet for each app, identified\n * by `dotNsIdentifier` (e.g., \"mark3t.dot\"). The user controls these accounts\n * but they are scoped to the requesting app.\n *\n * @param dotNsIdentifier - App identifier (e.g., \"mark3t.dot\").\n * @param derivationIndex - Derivation index within the app scope. Default: 0\n * @returns ResultAsync resolving to the account.\n */\n getProductAccount: (\n dotNsIdentifier: string,\n derivationIndex?: number,\n ) => ResultAsync<HostAccount, unknown>;\n\n /**\n * Get a signer for a product account.\n *\n * @param account - The product account.\n * @returns A PolkadotSigner for signing transactions.\n */\n getProductAccountSigner: (account: ProductAccount) => import(\"polkadot-api\").PolkadotSigner;\n\n /**\n * Get a contextual alias for a product account via Ring VRF.\n *\n * Aliases prove account membership in a ring without revealing which\n * account produced the alias.\n *\n * @param dotNsIdentifier - App identifier.\n * @param derivationIndex - Derivation index. Default: 0\n * @returns ResultAsync resolving to the contextual alias.\n */\n getProductAccountAlias: (\n dotNsIdentifier: string,\n derivationIndex?: number,\n ) => ResultAsync<ContextualAlias, unknown>;\n\n /**\n * Create a Ring VRF proof for anonymous operations.\n *\n * Proves that the signer is a member of the ring at the given location\n * without revealing which member.\n *\n * @param dotNsIdentifier - App identifier.\n * @param derivationIndex - Derivation index.\n * @param location - Ring location on-chain.\n * @param message - Message to sign.\n * @returns ResultAsync resolving to the proof bytes.\n */\n createRingVRFProof: (\n dotNsIdentifier: string,\n derivationIndex: number,\n location: unknown,\n message: Uint8Array,\n ) => ResultAsync<Uint8Array, unknown>;\n\n /**\n * Subscribe to account connection status changes.\n *\n * @param callback - Called with status string (\"connected\" | \"disconnected\").\n * @returns Unsubscribe handle.\n */\n subscribeAccountConnectionStatus: (\n callback: (status: string) => void,\n ) => { unsubscribe: () => void } | (() => void);\n}\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(\"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","/**\n * Higher-level wrapper for the host's single-permission flow.\n *\n * `hostApi.permission` takes a versioned envelope (`enumValue(\"v1\", ...)`)\n * and returns a neverthrow `ResultAsync` of an unwrapped versioned response.\n * Consumers rebuild that wrap/unwrap dance every time. `requestPermission`\n * collapses it to a one-liner that matches the shape of\n * {@link requestResourceAllocation} (throws on error, returns the unwrapped\n * payload on success).\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { enumValue, getTruApi, type RemotePermission } from \"./truapi.js\";\n\nconst log = createLogger(\"host:permissions\");\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(\n `requestPermission failed: ${err instanceof Error ? err.message : String(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: { permission?: (req: unknown) => unknown } | null,\n fn: (mod: typeof import(\"./permissions.js\")) => Promise<T>,\n ): Promise<T> {\n vi.resetModules();\n vi.doMock(\"./truapi.js\", () => ({\n getTruApi: async () => bridge,\n enumValue: (version: string, value: unknown) => ({ tag: version, value }),\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/);\n },\n );\n });\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parity/product-sdk-host",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Host container detection and storage access for Polkadot Desktop and Mobile environments",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/src/index.ts CHANGED
@@ -29,6 +29,8 @@ export {
29
29
  getTruApi,
30
30
  getPreimageManager,
31
31
  getAccountsProvider,
32
+ requestResourceAllocation,
33
+ createProofAuthorized,
32
34
  // Helpers from @novasamatech/host-api
33
35
  enumValue,
34
36
  isEnumVariant,
@@ -48,4 +50,14 @@ export type {
48
50
  ProductAccount,
49
51
  ContextualAlias,
50
52
  ResultAsync,
53
+ AllocatableResource,
54
+ AllocatableResourceTag,
55
+ AllocationOutcome,
56
+ AllocationOutcomeTag,
57
+ RemotePermission,
58
+ RemotePermissionTag,
59
+ Statement,
51
60
  } from "./truapi.js";
61
+
62
+ // Higher-level permission wrapper
63
+ export { requestPermission } from "./permissions.js";
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Higher-level wrapper for the host's single-permission flow.
3
+ *
4
+ * `hostApi.permission` takes a versioned envelope (`enumValue("v1", ...)`)
5
+ * and returns a neverthrow `ResultAsync` of an unwrapped versioned response.
6
+ * Consumers rebuild that wrap/unwrap dance every time. `requestPermission`
7
+ * collapses it to a one-liner that matches the shape of
8
+ * {@link requestResourceAllocation} (throws on error, returns the unwrapped
9
+ * payload on success).
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import { createLogger } from "@parity/product-sdk-logger";
15
+
16
+ import { enumValue, getTruApi, type RemotePermission } from "./truapi.js";
17
+
18
+ const log = createLogger("host:permissions");
19
+
20
+ /**
21
+ * Request a single remote permission from the host.
22
+ *
23
+ * Builds the `v1` envelope, calls `hostApi.permission`, unwraps the response,
24
+ * and returns the host's boolean granted/denied outcome.
25
+ *
26
+ * @param permission - The remote permission to request.
27
+ * @returns `true` if the host granted the permission, `false` if denied.
28
+ * @throws If the host is unavailable or the request fails.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const granted = await requestPermission({ tag: "ChainSubmit", value: undefined });
33
+ * if (!granted) {
34
+ * tellUserToReconnect();
35
+ * }
36
+ * ```
37
+ */
38
+ export async function requestPermission(permission: RemotePermission): Promise<boolean> {
39
+ const truApi = await getTruApi();
40
+ if (!truApi) {
41
+ throw new Error("requestPermission: TruAPI unavailable");
42
+ }
43
+ log.debug("requestPermission", { tag: permission.tag });
44
+
45
+ return await truApi.permission(enumValue("v1", permission)).match(
46
+ (envelope: { tag: "v1"; value: boolean }) => envelope.value,
47
+ (err: unknown) => {
48
+ throw new Error(
49
+ `requestPermission failed: ${err instanceof Error ? err.message : String(err)}`,
50
+ { cause: err },
51
+ );
52
+ },
53
+ );
54
+ }
55
+
56
+ if (import.meta.vitest) {
57
+ const { test, expect, describe, vi } = import.meta.vitest;
58
+
59
+ async function withMockedTruApi<T>(
60
+ bridge: { permission?: (req: unknown) => unknown } | null,
61
+ fn: (mod: typeof import("./permissions.js")) => Promise<T>,
62
+ ): Promise<T> {
63
+ vi.resetModules();
64
+ vi.doMock("./truapi.js", () => ({
65
+ getTruApi: async () => bridge,
66
+ enumValue: (version: string, value: unknown) => ({ tag: version, value }),
67
+ }));
68
+ try {
69
+ const mod = await import("./permissions.js");
70
+ return await fn(mod);
71
+ } finally {
72
+ vi.doUnmock("./truapi.js");
73
+ vi.resetModules();
74
+ }
75
+ }
76
+
77
+ describe("requestPermission", () => {
78
+ test("throws when TruAPI is unavailable", async () => {
79
+ await withMockedTruApi(null, async (mod) => {
80
+ await expect(
81
+ mod.requestPermission({ tag: "ChainSubmit", value: undefined }),
82
+ ).rejects.toThrow(/TruAPI unavailable/);
83
+ });
84
+ });
85
+
86
+ test("unwraps the v1 boolean outcome", async () => {
87
+ await withMockedTruApi(
88
+ {
89
+ permission: vi.fn().mockReturnValue({
90
+ match: async (onOk: (v: unknown) => unknown) =>
91
+ onOk({ tag: "v1", value: true }),
92
+ }),
93
+ },
94
+ async (mod) => {
95
+ const granted = await mod.requestPermission({
96
+ tag: "ChainSubmit",
97
+ value: undefined,
98
+ });
99
+ expect(granted).toBe(true);
100
+ },
101
+ );
102
+ });
103
+
104
+ test("wraps host errors with a diagnostic message", async () => {
105
+ await withMockedTruApi(
106
+ {
107
+ permission: vi.fn().mockReturnValue({
108
+ match: async (
109
+ _onOk: (v: unknown) => unknown,
110
+ onErr: (e: unknown) => unknown,
111
+ ) =>
112
+ onErr({
113
+ tag: "v1",
114
+ value: { name: "GenericError", message: "boom" },
115
+ }),
116
+ }),
117
+ },
118
+ async (mod) => {
119
+ await expect(
120
+ mod.requestPermission({ tag: "ChainSubmit", value: undefined }),
121
+ ).rejects.toThrow(/requestPermission failed/);
122
+ },
123
+ );
124
+ });
125
+ });
126
+ }
package/src/truapi.ts CHANGED
@@ -10,8 +10,35 @@
10
10
 
11
11
  import { createLogger } from "@parity/product-sdk-logger";
12
12
 
13
+ import { enumValue } from "@novasamatech/host-api";
14
+ import type {
15
+ AllocatableResource as AllocatableResourceCodec,
16
+ AllocationOutcome as AllocationOutcomeCodec,
17
+ CodecType,
18
+ RemotePermission as RemotePermissionCodec,
19
+ Statement as StatementCodec,
20
+ } from "@novasamatech/host-api";
21
+
22
+ import type { StatementProof } from "./types.js";
23
+
13
24
  const log = createLogger("host");
14
25
 
26
+ /**
27
+ * Extract a human-readable message from an unknown error. `JSON.stringify`
28
+ * on `Error` returns `"{}"` because `message` and `stack` are non-enumerable
29
+ * — without this helper, wire failures surface as `"... failed: {}"` with
30
+ * zero diagnostic context.
31
+ */
32
+ function formatError(err: unknown): string {
33
+ if (err instanceof Error) return err.message;
34
+ if (typeof err === "string") return err;
35
+ try {
36
+ return JSON.stringify(err);
37
+ } catch {
38
+ return String(err);
39
+ }
40
+ }
41
+
15
42
  // ─────────────────────────────────────────────────────────────────────────────
16
43
  // Helpers from @novasamatech/host-api (re-exported from @novasamatech/scale)
17
44
  // ─────────────────────────────────────────────────────────────────────────────
@@ -202,6 +229,160 @@ export async function getAccountsProvider(): Promise<AccountsProvider | null> {
202
229
  }
203
230
  }
204
231
 
232
+ // ─────────────────────────────────────────────────────────────────────────────
233
+ // Resource allocation
234
+ // ─────────────────────────────────────────────────────────────────────────────
235
+
236
+ /**
237
+ * Resource types requestable via {@link requestResourceAllocation}.
238
+ * Derived from the upstream codec so variant renames surface as compile
239
+ * errors, not runtime failures.
240
+ */
241
+ export type AllocatableResource = CodecType<typeof AllocatableResourceCodec>;
242
+
243
+ /** Tag-only view of {@link AllocatableResource} for places that just need the variant name. */
244
+ export type AllocatableResourceTag = AllocatableResource["tag"];
245
+
246
+ /**
247
+ * Per-resource outcome from {@link requestResourceAllocation}.
248
+ * The host strips secret payloads from `Allocated` before returning, so
249
+ * `value` is always `undefined` on the product side.
250
+ */
251
+ export type AllocationOutcome = CodecType<typeof AllocationOutcomeCodec>;
252
+
253
+ /** Tag-only view of {@link AllocationOutcome} (`"Allocated" | "Rejected" | "NotAvailable"`). */
254
+ export type AllocationOutcomeTag = AllocationOutcome["tag"];
255
+
256
+ /**
257
+ * Remote permission the dapp can ask the host to grant via
258
+ * {@link requestPermission}.
259
+ *
260
+ * Derived from the upstream codec so variant renames surface as compile
261
+ * errors, not runtime failures.
262
+ */
263
+ export type RemotePermission = CodecType<typeof RemotePermissionCodec>;
264
+
265
+ /** Tag-only view of {@link RemotePermission}. */
266
+ export type RemotePermissionTag = RemotePermission["tag"];
267
+
268
+ /**
269
+ * Request the host to pre-allocate one or more resource allowances.
270
+ *
271
+ * The host prompts the user once; subsequent operations covered by the
272
+ * granted allowance don't re-prompt.
273
+ *
274
+ * @param resources - Resources to request.
275
+ * @returns Per-resource outcomes in the same order as `resources`.
276
+ * @throws If the host is unavailable or the request fails.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * const outcomes = await requestResourceAllocation([
281
+ * { tag: "BulletInAllowance", value: undefined },
282
+ * ]);
283
+ * if (outcomes[0].tag === "Allocated") { ... }
284
+ * ```
285
+ */
286
+ export async function requestResourceAllocation(
287
+ resources: AllocatableResource[],
288
+ ): Promise<AllocationOutcome[]> {
289
+ const truApi = await getTruApi();
290
+ if (!truApi) {
291
+ throw new Error("requestResourceAllocation: TruAPI unavailable");
292
+ }
293
+ log.debug("requestResourceAllocation", { resources: resources.map((r) => r.tag) });
294
+
295
+ // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.
296
+ return await truApi.requestResourceAllocation(enumValue("v1", resources)).match(
297
+ (envelope: { tag: "v1"; value: AllocationOutcome[] }) => envelope.value,
298
+ (err: unknown) => {
299
+ throw new Error(`requestResourceAllocation failed: ${formatError(err)}`, {
300
+ cause: err,
301
+ });
302
+ },
303
+ );
304
+ }
305
+
306
+ // ─────────────────────────────────────────────────────────────────────────────
307
+ // Authorized Statement Store proof creation (RFC-10 §"Statement Store allowance")
308
+ // ─────────────────────────────────────────────────────────────────────────────
309
+
310
+ /**
311
+ * A Statement payload destined for the Statement Store. Matches the
312
+ * `pallet-statement` Statement structure.
313
+ *
314
+ * The optional `proof` field is the same {@link StatementProof} shape that
315
+ * {@link createProofAuthorized} returns: pass `undefined` here, call
316
+ * `createProofAuthorized` to obtain the proof, then attach it before
317
+ * submitting via `HostStatementStore.submit`. The `OnChain` variant of
318
+ * `StatementProof` is a chain-attestation reference; the `Sr25519` /
319
+ * `Ed25519` / `Ecdsa` variants are signing proofs.
320
+ *
321
+ * Derived from the upstream codec so structural changes surface as compile
322
+ * errors here, not runtime decode failures.
323
+ */
324
+ export type Statement = CodecType<typeof StatementCodec>;
325
+
326
+ /**
327
+ * Have the host sign a Statement using an allowance-bearing account it
328
+ * picks internally — RFC-10 §"Statement Store allowance".
329
+ *
330
+ * The product passes only the Statement payload; the host chooses the
331
+ * `//allowance//statement-store//{productId}` account that holds SSS
332
+ * allowance and signs with it. Allowance is provisioned implicitly on
333
+ * first use if the host hasn't already pre-allocated via
334
+ * {@link requestResourceAllocation}; products never see the signing
335
+ * account or its key material.
336
+ *
337
+ * Pairs with {@link getStatementStore}'s `submit`: call this to obtain
338
+ * a proof, attach it to the Statement, and submit the result.
339
+ *
340
+ * @param statement - The Statement to be signed.
341
+ * @returns The proof to attach before submitting.
342
+ * @throws If the host is unavailable or the host-side signing fails.
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * import { createProofAuthorized, getStatementStore } from "@parity/product-sdk-host";
347
+ *
348
+ * const statement = {
349
+ * proof: undefined,
350
+ * decryptionKey: undefined,
351
+ * expiry: undefined,
352
+ * channel: undefined,
353
+ * topics: [],
354
+ * data: payload,
355
+ * };
356
+ * const proof = await createProofAuthorized(statement);
357
+ * const store = await getStatementStore();
358
+ * await store?.submit({ ...statement, proof });
359
+ * ```
360
+ *
361
+ * @remarks
362
+ * RFC-10 introduces this as a new, strictly additive TruAPI call. The
363
+ * pre-existing `HostStatementStore.createProof(accountId, statement)`
364
+ * surface stays available for products that own a non-allowance signing
365
+ * account; this wrapper is the sponsored-submission path.
366
+ */
367
+ export async function createProofAuthorized(statement: Statement): Promise<StatementProof> {
368
+ const truApi = await getTruApi();
369
+ if (!truApi) {
370
+ throw new Error("createProofAuthorized: TruAPI unavailable");
371
+ }
372
+ log.debug("createProofAuthorized", {
373
+ topics: statement.topics.length,
374
+ dataLen: statement.data?.length ?? 0,
375
+ });
376
+
377
+ // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.
378
+ return await truApi.statementStoreCreateProofAuthorized(enumValue("v1", statement)).match(
379
+ (envelope: { tag: "v1"; value: StatementProof }) => envelope.value,
380
+ (err: unknown) => {
381
+ throw new Error(`createProofAuthorized failed: ${formatError(err)}`, { cause: err });
382
+ },
383
+ );
384
+ }
385
+
205
386
  /**
206
387
  * One of the user's existing wallet accounts, surfaced through the host and
207
388
  * identified by its public key and an optional name. Contrast with
@@ -376,4 +557,35 @@ if (import.meta.vitest) {
376
557
  const { enumValue } = await import("./truapi.js");
377
558
  expect(typeof enumValue).toBe("function");
378
559
  });
560
+
561
+ test("requestResourceAllocation throws when TruAPI is unavailable", async () => {
562
+ cachedTruApi = null;
563
+ const api = await getTruApi();
564
+ if (api === null) {
565
+ await expect(
566
+ requestResourceAllocation([{ tag: "BulletInAllowance", value: undefined }]),
567
+ ).rejects.toThrow(/TruAPI unavailable/);
568
+ } else {
569
+ expect(typeof requestResourceAllocation).toBe("function");
570
+ }
571
+ });
572
+
573
+ test("createProofAuthorized throws when TruAPI is unavailable", async () => {
574
+ cachedTruApi = null;
575
+ const api = await getTruApi();
576
+ if (api === null) {
577
+ await expect(
578
+ createProofAuthorized({
579
+ proof: undefined,
580
+ decryptionKey: undefined,
581
+ expiry: undefined,
582
+ channel: undefined,
583
+ topics: [],
584
+ data: undefined,
585
+ }),
586
+ ).rejects.toThrow(/TruAPI unavailable/);
587
+ } else {
588
+ expect(typeof createProofAuthorized).toBe("function");
589
+ }
590
+ });
379
591
  }