@novasamatech/host-papp 0.4.0-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.
Files changed (48) hide show
  1. package/README.md +11 -0
  2. package/dist/adapters/identity/rpc.d.ts +4 -0
  3. package/dist/adapters/identity/rpc.js +31 -0
  4. package/dist/adapters/identity/types.d.ts +8 -0
  5. package/dist/adapters/identity/types.js +1 -0
  6. package/dist/adapters/lazyClient/papi.d.ts +3 -0
  7. package/dist/adapters/lazyClient/papi.js +12 -0
  8. package/dist/adapters/lazyClient/types.d.ts +4 -0
  9. package/dist/adapters/lazyClient/types.js +1 -0
  10. package/dist/adapters/statement/rpc.d.ts +3 -0
  11. package/dist/adapters/statement/rpc.js +45 -0
  12. package/dist/adapters/statement/types.d.ts +6 -0
  13. package/dist/adapters/statement/types.js +1 -0
  14. package/dist/adapters/storage/localStorage.d.ts +2 -0
  15. package/dist/adapters/storage/localStorage.js +12 -0
  16. package/dist/adapters/storage/memory.d.ts +2 -0
  17. package/dist/adapters/storage/memory.js +12 -0
  18. package/dist/adapters/storage/types.d.ts +4 -0
  19. package/dist/adapters/storage/types.js +1 -0
  20. package/dist/adapters/transport/rpc.d.ts +3 -0
  21. package/dist/adapters/transport/rpc.js +51 -0
  22. package/dist/adapters/transport/types.d.ts +6 -0
  23. package/dist/adapters/transport/types.js +1 -0
  24. package/dist/constants.d.ts +1 -0
  25. package/dist/constants.js +1 -0
  26. package/dist/helpers/utils.d.ts +1 -0
  27. package/dist/helpers/utils.js +3 -0
  28. package/dist/helpers.d.ts +1 -0
  29. package/dist/helpers.js +3 -0
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +7 -0
  32. package/dist/modules/accounts.d.ts +1 -0
  33. package/dist/modules/accounts.js +2 -0
  34. package/dist/modules/crypto.d.ts +24 -0
  35. package/dist/modules/crypto.js +73 -0
  36. package/dist/modules/secretStorage.d.ts +14 -0
  37. package/dist/modules/secretStorage.js +53 -0
  38. package/dist/modules/signIn.d.ts +46 -0
  39. package/dist/modules/signIn.js +191 -0
  40. package/dist/modules/statementStore.d.ts +13 -0
  41. package/dist/modules/statementStore.js +20 -0
  42. package/dist/papp.d.ts +25 -0
  43. package/dist/papp.js +54 -0
  44. package/dist/structs.d.ts +24 -0
  45. package/dist/structs.js +32 -0
  46. package/dist/types.d.ts +6 -0
  47. package/dist/types.js +1 -0
  48. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @novasamatech/host-papp
2
+
3
+ Polkadot app integration layer for host applications.
4
+
5
+ ## Overview
6
+
7
+ ## Installation
8
+
9
+ ```shell
10
+ npm install @novasamatech/host-papp --save -E
11
+ ```
@@ -0,0 +1,4 @@
1
+ import type { LazyClientAdapter } from '../lazyClient/types.js';
2
+ import type { StorageAdapter } from '../storage/types.js';
3
+ import type { IdentityAdapter } from './types.js';
4
+ export declare const createIdentityRpcAdapter: (lazyClient: LazyClientAdapter, storage: StorageAdapter) => IdentityAdapter;
@@ -0,0 +1,31 @@
1
+ import { AccountId } from 'polkadot-api';
2
+ export const createIdentityRpcAdapter = (lazyClient, storage) => {
3
+ const getKey = (accountId) => `identity_${accountId}`;
4
+ const accCodec = AccountId();
5
+ return {
6
+ async getIdentity(accountId) {
7
+ const existingIdentity = await storage.read(getKey(accountId));
8
+ if (existingIdentity) {
9
+ try {
10
+ return JSON.parse(existingIdentity);
11
+ }
12
+ catch (error) {
13
+ console.error('Error while reading identity from cache', error);
14
+ }
15
+ }
16
+ const client = lazyClient.getClient();
17
+ const unsafeApi = client.getUnsafeApi();
18
+ const result = await unsafeApi.query.Resources?.Consumers?.getValue(accCodec.dec(accountId));
19
+ if (result) {
20
+ const identity = {
21
+ fullUsername: result.full_username ? result.full_username.asText() : null,
22
+ liteUsername: result.lite_username ? result.lite_username.asText() : null,
23
+ credibility: result.credibility.type,
24
+ };
25
+ await storage.write(getKey(accountId), JSON.stringify(identity));
26
+ return identity;
27
+ }
28
+ return null;
29
+ },
30
+ };
31
+ };
@@ -0,0 +1,8 @@
1
+ export type Identity = {
2
+ fullUsername: string | null;
3
+ liteUsername: string;
4
+ credibility: string;
5
+ };
6
+ export type IdentityAdapter = {
7
+ getIdentity(accountId: string): Promise<Identity | null>;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
2
+ import type { LazyClientAdapter } from './types.js';
3
+ export declare const createPapiLazyClient: (provider: JsonRpcProvider) => LazyClientAdapter;
@@ -0,0 +1,12 @@
1
+ import { createClient } from 'polkadot-api';
2
+ export const createPapiLazyClient = (provider) => {
3
+ let client = null;
4
+ return {
5
+ getClient() {
6
+ if (!client) {
7
+ client = createClient(provider);
8
+ }
9
+ return client;
10
+ },
11
+ };
12
+ };
@@ -0,0 +1,4 @@
1
+ import type { PolkadotClient } from 'polkadot-api';
2
+ export type LazyClientAdapter = {
3
+ getClient(): PolkadotClient;
4
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { LazyClientAdapter } from '../lazyClient/types.js';
2
+ import type { StatementAdapter } from './types.js';
3
+ export declare function createPapiStatementAdapter(lazyClient: LazyClientAdapter): StatementAdapter;
@@ -0,0 +1,45 @@
1
+ import { createStatementSdk } from '@polkadot-api/sdk-statement';
2
+ import { FixedSizeBinary } from '@polkadot-api/substrate-bindings';
3
+ export function createPapiStatementAdapter(lazyClient) {
4
+ const POLLING_INTERVAL = 1000;
5
+ const sdk = createStatementSdk((method, params) => {
6
+ const client = lazyClient.getClient();
7
+ return client._request(method, params);
8
+ });
9
+ const transportProvider = {
10
+ getStatements(topics) {
11
+ // @ts-expect-error lib versions mismatch
12
+ return sdk.getStatements({ topics: topics.map(topic => new FixedSizeBinary(topic)) });
13
+ },
14
+ subscribeStatements(topics, callback) {
15
+ return polling(POLLING_INTERVAL, () => transportProvider.getStatements(topics), callback);
16
+ },
17
+ submitStatement(statement) {
18
+ return sdk.submit(statement);
19
+ },
20
+ };
21
+ return transportProvider;
22
+ }
23
+ function polling(interval, request, callback) {
24
+ let active = true;
25
+ let tm = null;
26
+ function createCycle() {
27
+ tm = setTimeout(() => {
28
+ if (!active) {
29
+ return;
30
+ }
31
+ request()
32
+ .then(callback)
33
+ .finally(() => {
34
+ createCycle();
35
+ });
36
+ }, interval);
37
+ }
38
+ createCycle();
39
+ return () => {
40
+ active = false;
41
+ if (tm !== null) {
42
+ clearTimeout(tm);
43
+ }
44
+ };
45
+ }
@@ -0,0 +1,6 @@
1
+ import type { SignedStatement, Statement } from '@polkadot-api/sdk-statement';
2
+ export type StatementAdapter = {
3
+ getStatements(topics: Uint8Array[]): Promise<Statement[]>;
4
+ subscribeStatements(topics: Uint8Array[], callback: (response: Statement[]) => unknown): VoidFunction;
5
+ submitStatement(statement: SignedStatement): Promise<void>;
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { StorageAdapter } from './types.js';
2
+ export declare function createLocalStorageAdapter(prefix: string): StorageAdapter;
@@ -0,0 +1,12 @@
1
+ export function createLocalStorageAdapter(prefix) {
2
+ const withPrefix = (key) => `PAPP_${prefix}_${key}`;
3
+ return {
4
+ async write(key, value) {
5
+ localStorage.setItem(withPrefix(key), value);
6
+ return true;
7
+ },
8
+ async read(key) {
9
+ return localStorage.getItem(withPrefix(key));
10
+ },
11
+ };
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { StorageAdapter } from './types.js';
2
+ export declare function createMemoryAdapter(external?: Record<string, string>): StorageAdapter;
@@ -0,0 +1,12 @@
1
+ export function createMemoryAdapter(external) {
2
+ const storage = external ? { ...external } : {};
3
+ return {
4
+ async write(key, value) {
5
+ storage[key] = value;
6
+ return true;
7
+ },
8
+ async read(key) {
9
+ return storage[key] ?? null;
10
+ },
11
+ };
12
+ }
@@ -0,0 +1,4 @@
1
+ export type StorageAdapter = {
2
+ write(key: string, value: string): Promise<boolean>;
3
+ read(key: string): Promise<string | null>;
4
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
2
+ import type { Transport } from './types.js';
3
+ export declare function createRpcTransport(rpcProvider: JsonRpcProvider): Transport;
@@ -0,0 +1,51 @@
1
+ import { createClient } from '@polkadot-api/raw-client';
2
+ import { createStatementSdk } from '@polkadot-api/sdk-statement';
3
+ import { FixedSizeBinary } from '@polkadot-api/substrate-bindings';
4
+ export function createRpcTransport(rpcProvider) {
5
+ const POLLING_INTERVAL = 1000;
6
+ const client = createClient(rpcProvider);
7
+ const sdk = createStatementSdk((method, params) => {
8
+ return new Promise((resolve, reject) => {
9
+ client.request(method, params, {
10
+ onSuccess: resolve,
11
+ onError: reject,
12
+ });
13
+ });
14
+ });
15
+ const transportProvider = {
16
+ getStatements(topics) {
17
+ // @ts-expect-error lib versions mismatch
18
+ return sdk.getStatements({ topics: topics.map(topic => new FixedSizeBinary(topic)) });
19
+ },
20
+ subscribeStatements(topics, callback) {
21
+ return polling(POLLING_INTERVAL, () => transportProvider.getStatements(topics), callback);
22
+ },
23
+ submitStatement(statement) {
24
+ return sdk.submit(statement);
25
+ },
26
+ };
27
+ return transportProvider;
28
+ }
29
+ function polling(interval, request, callback) {
30
+ let active = true;
31
+ let tm = null;
32
+ function createCycle() {
33
+ tm = setTimeout(() => {
34
+ if (!active) {
35
+ return;
36
+ }
37
+ request()
38
+ .then(callback)
39
+ .finally(() => {
40
+ createCycle();
41
+ });
42
+ }, interval);
43
+ }
44
+ createCycle();
45
+ return () => {
46
+ active = false;
47
+ if (tm !== null) {
48
+ clearTimeout(tm);
49
+ }
50
+ };
51
+ }
@@ -0,0 +1,6 @@
1
+ import type { SignedStatement, Statement } from '@polkadot-api/sdk-statement';
2
+ export type Transport = {
3
+ getStatements(topics: Uint8Array[]): Promise<Statement[]>;
4
+ subscribeStatements(topics: Uint8Array[], callback: (response: Statement[]) => unknown): VoidFunction;
5
+ submitStatement(statement: SignedStatement): Promise<void>;
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const SS_ENDPOINTS: string[];
@@ -0,0 +1 @@
1
+ export const SS_ENDPOINTS = ['wss://pop-testnet.parity-lab.parity.io:443/9910'];
@@ -0,0 +1 @@
1
+ export declare function isAbortError(err: object): boolean;
@@ -0,0 +1,3 @@
1
+ export function isAbortError(err) {
2
+ return err && 'name' in err && err.name === 'AbortError';
3
+ }
@@ -0,0 +1 @@
1
+ export declare function isAbortError(err: object): boolean;
@@ -0,0 +1,3 @@
1
+ export function isAbortError(err) {
2
+ return err && 'name' in err && err.name === 'AbortError';
3
+ }
@@ -0,0 +1,4 @@
1
+ export type { PappAdapter } from './papp.js';
2
+ export type { SignInStatus } from './modules/signIn.js';
3
+ export type { Identity } from './adapters/identity/types.js';
4
+ export declare function createPappHostAdapter(appId: string, metadata: string): import("./papp.js").PappAdapter;
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { createPappAdapter } from './papp.js';
2
+ export function createPappHostAdapter(appId, metadata) {
3
+ return createPappAdapter({
4
+ appId,
5
+ metadata,
6
+ });
7
+ }
@@ -0,0 +1 @@
1
+ export declare function getAccountsFlow(): void;
@@ -0,0 +1,2 @@
1
+ // TODO implement
2
+ export function getAccountsFlow() { }
@@ -0,0 +1,24 @@
1
+ import type { Codec } from 'scale-ts';
2
+ import type { Branded } from '../types.js';
3
+ export type SsPublicKey = Branded<Uint8Array, 'SsPublicKey'>;
4
+ export type SsSecret = Branded<Uint8Array, 'SsSecret'>;
5
+ export type EncrPublicKey = Branded<Uint8Array, 'EncrPublicKey'>;
6
+ export type EncrSecret = Branded<Uint8Array, 'EncrSecret'>;
7
+ export type SharedSession = Branded<Uint8Array, 'SharedSession'>;
8
+ export declare const SsPubKey: Codec<SsPublicKey>;
9
+ export declare const EncrPubKey: Codec<EncrPublicKey>;
10
+ export declare function stringToBytes(str: string): Uint8Array<ArrayBuffer>;
11
+ export declare function bytesToString(bytes: Uint8Array): string;
12
+ export declare function mergeBytes(...bytes: Uint8Array[]): Uint8Array<ArrayBuffer>;
13
+ export declare const SS_SECRET_SEED_SIZE = 32;
14
+ export declare function createSsSecret(seed: Uint8Array): SsSecret;
15
+ export declare function getSsPub(secret: SsSecret): SsPublicKey;
16
+ export declare const ENCR_SECRET_SEED_SIZE = 48;
17
+ export declare function createEncrSecret(seed: Uint8Array): EncrSecret;
18
+ export declare function getEncrPub(secret: EncrSecret): EncrPublicKey;
19
+ export declare function createRandomSeed(suffix: string, size: number): Uint8Array<ArrayBufferLike>;
20
+ export declare function createStableSeed(value: string, size: number): Uint8Array<ArrayBufferLike>;
21
+ export declare function khash(secret: Uint8Array, message: Uint8Array): Uint8Array<ArrayBufferLike>;
22
+ export declare function createSharedSecret(secret: EncrSecret, publicKey: Uint8Array): Uint8Array<ArrayBufferLike>;
23
+ export declare function createSymmetricKey(sharedSecret: Uint8Array): Uint8Array<ArrayBuffer>;
24
+ export declare function decrypt(secret: Uint8Array, message: Uint8Array): Uint8Array<ArrayBufferLike>;
@@ -0,0 +1,73 @@
1
+ import { gcm } from '@noble/ciphers/aes.js';
2
+ import { p256 } from '@noble/curves/nist.js';
3
+ import { blake2b } from '@noble/hashes/blake2.js';
4
+ import { hkdf } from '@noble/hashes/hkdf.js';
5
+ import { sha256 } from '@noble/hashes/sha2.js';
6
+ import { randomBytes } from '@noble/hashes/utils.js';
7
+ import { getPublicKey as sr25519GetPublicKey, secretFromSeed as sr25519SecretFromSeed } from '@scure/sr25519';
8
+ import { Bytes } from 'scale-ts';
9
+ // schemas
10
+ function brandedBytesCodec(length) {
11
+ return Bytes(length);
12
+ }
13
+ export const SsPubKey = brandedBytesCodec(32);
14
+ export const EncrPubKey = brandedBytesCodec(65);
15
+ // helpers
16
+ const textEncoder = new TextEncoder();
17
+ const textDecoder = new TextDecoder();
18
+ export function stringToBytes(str) {
19
+ return textEncoder.encode(str);
20
+ }
21
+ export function bytesToString(bytes) {
22
+ return textDecoder.decode(bytes);
23
+ }
24
+ export function mergeBytes(...bytes) {
25
+ const len = bytes.reduce((l, b) => l + b.length, 0);
26
+ const merged = new Uint8Array(len);
27
+ let offset = 0;
28
+ for (const arr of bytes) {
29
+ merged.set(arr, offset);
30
+ offset += arr.length;
31
+ }
32
+ return merged;
33
+ }
34
+ // statement store key pair
35
+ export const SS_SECRET_SEED_SIZE = 32;
36
+ export function createSsSecret(seed) {
37
+ return sr25519SecretFromSeed(seed);
38
+ }
39
+ export function getSsPub(secret) {
40
+ return sr25519GetPublicKey(secret);
41
+ }
42
+ // encryption key pair
43
+ export const ENCR_SECRET_SEED_SIZE = 48;
44
+ export function createEncrSecret(seed) {
45
+ const { secretKey } = p256.keygen(seed);
46
+ return secretKey;
47
+ }
48
+ export function getEncrPub(secret) {
49
+ return p256.getPublicKey(secret, false);
50
+ }
51
+ // helpers
52
+ export function createRandomSeed(suffix, size) {
53
+ return blake2b(mergeBytes(randomBytes(128), stringToBytes(suffix)), { dkLen: size });
54
+ }
55
+ export function createStableSeed(value, size) {
56
+ return blake2b(stringToBytes(value), { dkLen: size });
57
+ }
58
+ export function khash(secret, message) {
59
+ return blake2b(message, { dkLen: 256 / 8, key: secret });
60
+ }
61
+ export function createSharedSecret(secret, publicKey) {
62
+ return p256.getSharedSecret(secret, publicKey);
63
+ }
64
+ export function createSymmetricKey(sharedSecret) {
65
+ return sharedSecret.slice(1, 33);
66
+ }
67
+ export function decrypt(secret, message) {
68
+ const nonce = message.slice(0, 12);
69
+ const cipherText = message.slice(12);
70
+ const aesKey = hkdf(sha256, secret, new Uint8Array(), new Uint8Array(), 32);
71
+ const aes = gcm(aesKey, nonce);
72
+ return aes.decrypt(cipherText);
73
+ }
@@ -0,0 +1,14 @@
1
+ import type { StorageAdapter } from '../adapters/storage/types.js';
2
+ import type { SessionTopic } from '../types.js';
3
+ import type { EncrSecret, SsSecret } from './crypto.js';
4
+ export type SecretStorage = {
5
+ readSsSecret(): Promise<SsSecret | null>;
6
+ writeSsSecret(value: SsSecret): Promise<boolean>;
7
+ readEncrSecret(): Promise<EncrSecret | null>;
8
+ writeEncrSecret(value: EncrSecret): Promise<boolean>;
9
+ readSessionTopic(): Promise<SessionTopic | null>;
10
+ writeSessionTopic(value: SessionTopic): Promise<boolean>;
11
+ readPappAccountId(): Promise<string | null>;
12
+ writePappAccountId(value: string): Promise<boolean>;
13
+ };
14
+ export declare function createSecretStorage(appId: string, storage: StorageAdapter): SecretStorage;
@@ -0,0 +1,53 @@
1
+ import { gcm } from '@noble/ciphers/aes.js';
2
+ import { blake2b } from '@noble/hashes/blake2.js';
3
+ import { fromHex, toHex } from '@polkadot-api/utils';
4
+ import { bytesToString, stringToBytes } from './crypto.js';
5
+ export function createSecretStorage(appId, storage) {
6
+ const ssSecret = rwBytes('SsSecret', appId, storage);
7
+ const encrSecret = rwBytes('EncrSecret', appId, storage);
8
+ const sessionTopic = rwBytes('SessionTopic', appId, storage);
9
+ const pappAccountId = rwString('PappAccountId', appId, storage);
10
+ return {
11
+ readSsSecret: ssSecret.read,
12
+ writeSsSecret: ssSecret.write,
13
+ readEncrSecret: encrSecret.read,
14
+ writeEncrSecret: encrSecret.write,
15
+ readSessionTopic: sessionTopic.read,
16
+ writeSessionTopic: sessionTopic.write,
17
+ readPappAccountId: pappAccountId.read,
18
+ writePappAccountId: pappAccountId.write,
19
+ };
20
+ }
21
+ const withAppId = (appId, key) => `${appId}_${key}`;
22
+ const rwBytes = (key, appId, storage) => ({
23
+ async read() {
24
+ const value = await storage.read(withAppId(appId, key));
25
+ if (value) {
26
+ const aes = getAes(appId);
27
+ return aes.decrypt(fromHex(value));
28
+ }
29
+ else {
30
+ return null;
31
+ }
32
+ },
33
+ write(value) {
34
+ const aes = getAes(appId);
35
+ return storage.write(withAppId(appId, key), toHex(aes.encrypt(value)));
36
+ },
37
+ });
38
+ const rwString = (key, appId, storage) => {
39
+ const bytes = rwBytes(key, appId, storage);
40
+ return {
41
+ async read() {
42
+ const value = await bytes.read();
43
+ return value ? bytesToString(value) : null;
44
+ },
45
+ write(value) {
46
+ const b = stringToBytes(value);
47
+ return bytes.write(b);
48
+ },
49
+ };
50
+ };
51
+ function getAes(appId) {
52
+ return gcm(blake2b(stringToBytes(appId), { dkLen: 16 }), blake2b(stringToBytes('nonce'), { dkLen: 32 }));
53
+ }
@@ -0,0 +1,46 @@
1
+ import type { StatementAdapter } from '../adapters/statement/types.js';
2
+ import type { StorageAdapter } from '../adapters/storage/types.js';
3
+ import type { SessionTopic } from '../types.js';
4
+ import type { EncrPublicKey, SsPublicKey } from './crypto.js';
5
+ export declare const HandshakeData: import("scale-ts").Codec<{
6
+ tag: "V1";
7
+ value: [SsPublicKey, EncrPublicKey, string];
8
+ }>;
9
+ export declare const HandshakeResponsePayload: import("scale-ts").Codec<{
10
+ tag: "V1";
11
+ value: [Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>];
12
+ }>;
13
+ export declare const HandshakeResponseSensitiveData: import("scale-ts").Codec<[Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>]>;
14
+ export type SignInStatus = {
15
+ step: 'none';
16
+ } | {
17
+ step: 'initial';
18
+ } | {
19
+ step: 'pairing';
20
+ payload: string;
21
+ } | {
22
+ step: 'error';
23
+ message: string;
24
+ } | {
25
+ step: 'finished';
26
+ sessionTopic: SessionTopic;
27
+ pappAccountId: string;
28
+ };
29
+ export type SignInResult = {
30
+ sessionTopic: SessionTopic;
31
+ pappAccountId: string;
32
+ };
33
+ type Params = {
34
+ appId: string;
35
+ metadata: string;
36
+ statements: StatementAdapter;
37
+ storage: StorageAdapter;
38
+ };
39
+ export declare function createSignInFlow({ appId, metadata, statements, storage }: Params): {
40
+ getSignedUser(): Promise<SignInResult | null>;
41
+ signIn(): Promise<SignInResult | null>;
42
+ abortSignIn(): void;
43
+ getSignInStatus(): SignInStatus;
44
+ onStatusChange(callback: (status: SignInStatus) => void): import("nanoevents").Unsubscribe;
45
+ };
46
+ export {};
@@ -0,0 +1,191 @@
1
+ import { toHex } from '@polkadot-api/utils';
2
+ import { createNanoEvents } from 'nanoevents';
3
+ import { Bytes, Enum, Tuple, str } from 'scale-ts';
4
+ import { isAbortError } from '../helpers/utils.js';
5
+ import { ENCR_SECRET_SEED_SIZE, EncrPubKey, SS_SECRET_SEED_SIZE, SsPubKey, createEncrSecret, createSharedSecret, createSsSecret, createStableSeed, createSymmetricKey, decrypt, getEncrPub, getSsPub, khash, mergeBytes, stringToBytes, } from './crypto.js';
6
+ import { createSecretStorage } from './secretStorage.js';
7
+ import { createSession } from './statementStore.js';
8
+ // codecs
9
+ export const HandshakeData = Enum({
10
+ V1: Tuple(SsPubKey, EncrPubKey, str),
11
+ });
12
+ export const HandshakeResponsePayload = Enum({
13
+ // [encrypted, tmp_key]
14
+ V1: Tuple(Bytes(), Bytes(65)),
15
+ });
16
+ export const HandshakeResponseSensitiveData = Tuple(Bytes(65), Bytes(32));
17
+ export function createSignInFlow({ appId, metadata, statements, storage }) {
18
+ const secretStorage = createSecretStorage(appId, storage);
19
+ const events = createNanoEvents();
20
+ let signInStatus = { step: 'none' };
21
+ events.on('status', status => {
22
+ signInStatus = status;
23
+ });
24
+ let signInPromise = null;
25
+ let abort = null;
26
+ const signInFlow = {
27
+ getSignedUser() {
28
+ return Promise.all([secretStorage.readSessionTopic(), secretStorage.readPappAccountId()]).then(([existingSessionTopic, existingPappAccountId]) => {
29
+ if (existingSessionTopic && existingPappAccountId) {
30
+ events.emit('status', {
31
+ step: 'finished',
32
+ sessionTopic: existingSessionTopic,
33
+ pappAccountId: existingPappAccountId,
34
+ });
35
+ return {
36
+ sessionTopic: existingSessionTopic,
37
+ pappAccountId: existingPappAccountId,
38
+ };
39
+ }
40
+ return null;
41
+ });
42
+ },
43
+ async signIn() {
44
+ if (signInPromise) {
45
+ return signInPromise;
46
+ }
47
+ abort = new AbortController();
48
+ events.emit('status', { step: 'initial' });
49
+ signInPromise = signInFlow
50
+ .getSignedUser()
51
+ .then(async (signedIn) => {
52
+ if (signedIn) {
53
+ events.emit('status', {
54
+ step: 'finished',
55
+ sessionTopic: signedIn.sessionTopic,
56
+ pappAccountId: signedIn.pappAccountId,
57
+ });
58
+ return signedIn;
59
+ }
60
+ const { ssPublicKey, encrPublicKey, encrSecret } = await getSecretKeys(appId, secretStorage);
61
+ const handshakeTopic = createHandshakeTopic({ encrPublicKey, ssPublicKey });
62
+ const handshakePayload = createHandshakePayload({ ssPublicKey, encrPublicKey, metadata });
63
+ events.emit('status', { step: 'pairing', payload: createDeeplink(handshakePayload) });
64
+ return waitForStatements(statements, handshakeTopic, abort?.signal ?? null, (statements, resolve) => {
65
+ for (const statement of [...statements].reverse()) {
66
+ if (!statement.data)
67
+ continue;
68
+ const { sessionTopic, pappAccountId } = retrieveSessionTopic({
69
+ payload: statement.data.asBytes(),
70
+ encrSecret,
71
+ ssPublicKey,
72
+ });
73
+ resolve({ sessionTopic, pappAccountId: toHex(pappAccountId) });
74
+ break;
75
+ }
76
+ });
77
+ })
78
+ .then(async ({ sessionTopic, pappAccountId }) => {
79
+ await secretStorage.writeSessionTopic(sessionTopic);
80
+ await secretStorage.writePappAccountId(pappAccountId);
81
+ events.emit('status', { step: 'finished', sessionTopic, pappAccountId });
82
+ return { sessionTopic, pappAccountId };
83
+ })
84
+ .catch(e => {
85
+ if (isAbortError(e)) {
86
+ events.emit('status', { step: 'none' });
87
+ return null;
88
+ }
89
+ events.emit('status', { step: 'error', message: e.message });
90
+ throw e;
91
+ });
92
+ return signInPromise;
93
+ },
94
+ abortSignIn() {
95
+ if (abort) {
96
+ events.emit('status', { step: 'none' });
97
+ abort.abort();
98
+ }
99
+ },
100
+ getSignInStatus() {
101
+ return signInStatus;
102
+ },
103
+ onStatusChange(callback) {
104
+ return events.on('status', callback);
105
+ },
106
+ };
107
+ return signInFlow;
108
+ }
109
+ function createHandshakeTopic({ encrPublicKey, ssPublicKey, }) {
110
+ return khash(ssPublicKey, mergeBytes(encrPublicKey, stringToBytes('topic')));
111
+ }
112
+ function createHandshakePayload({ encrPublicKey, ssPublicKey, metadata, }) {
113
+ return HandshakeData.enc({
114
+ tag: 'V1',
115
+ value: [ssPublicKey, encrPublicKey, metadata],
116
+ });
117
+ }
118
+ function parseHandshakePayload(payload) {
119
+ const decoded = HandshakeResponsePayload.dec(payload);
120
+ switch (decoded.tag) {
121
+ case 'V1':
122
+ return {
123
+ encrypted: decoded.value[0],
124
+ tmpKey: decoded.value[1],
125
+ };
126
+ default:
127
+ throw new Error('Unsupported handshake payload version');
128
+ }
129
+ }
130
+ function retrieveSessionTopic({ payload, encrSecret, ssPublicKey, }) {
131
+ const { encrypted, tmpKey } = parseHandshakePayload(payload);
132
+ const symmetricKey = createSymmetricKey(createSharedSecret(encrSecret, tmpKey));
133
+ const decrypted = decrypt(symmetricKey, encrypted);
134
+ console.log('decrypted', decrypted.length, 65 + 32); // true
135
+ const [pappEncrPublicKey, userPublicKey] = HandshakeResponseSensitiveData.dec(decrypted);
136
+ const sharedSecret = createSharedSecret(encrSecret, pappEncrPublicKey);
137
+ const session = createSession({
138
+ sharedSecret: sharedSecret,
139
+ accountA: ssPublicKey,
140
+ accountB: pappEncrPublicKey,
141
+ });
142
+ console.log('userPublicKey', userPublicKey.length, toHex(userPublicKey));
143
+ console.log('sessionTopic', session.a.length, toHex(session.a));
144
+ return {
145
+ pappAccountId: userPublicKey,
146
+ sessionTopic: session.a,
147
+ };
148
+ }
149
+ async function getSecretKeys(appId, secretStorage) {
150
+ let ssSecret = await secretStorage.readSsSecret();
151
+ if (!ssSecret) {
152
+ // TODO randomize seed
153
+ // For testing purpose only
154
+ const seed = createStableSeed(appId, SS_SECRET_SEED_SIZE);
155
+ ssSecret = createSsSecret(seed);
156
+ await secretStorage.writeSsSecret(ssSecret);
157
+ }
158
+ let encrSecret = await secretStorage.readEncrSecret();
159
+ if (!encrSecret) {
160
+ // TODO randomize seed
161
+ // For testing purpose only
162
+ const seed = createStableSeed(appId, ENCR_SECRET_SEED_SIZE);
163
+ encrSecret = createEncrSecret(seed);
164
+ await secretStorage.writeEncrSecret(encrSecret);
165
+ }
166
+ const ssPublicKey = getSsPub(ssSecret);
167
+ const encrPublicKey = getEncrPub(encrSecret);
168
+ return { ssPublicKey, encrPublicKey, ssSecret, encrSecret };
169
+ }
170
+ function createDeeplink(payload) {
171
+ return `polkadotapp://pair?handshake=${toHex(payload)}`;
172
+ }
173
+ function waitForStatements(transport, topic, abortSignal, callback) {
174
+ return new Promise((resolve, reject) => {
175
+ const unsubscribe = transport.subscribeStatements([topic], statements => {
176
+ if (abortSignal?.aborted) {
177
+ unsubscribe();
178
+ try {
179
+ abortSignal.throwIfAborted();
180
+ }
181
+ catch (e) {
182
+ reject(e);
183
+ }
184
+ }
185
+ callback(statements, value => {
186
+ unsubscribe();
187
+ resolve(value);
188
+ });
189
+ });
190
+ });
191
+ }
@@ -0,0 +1,13 @@
1
+ import type { UnsignedStatement } from '@polkadot-api/sdk-statement';
2
+ import type { SsSecret } from './crypto.js';
3
+ export declare function createSession({ sharedSecret, accountA, accountB, pinA, pinB, }: {
4
+ sharedSecret: Uint8Array;
5
+ accountA: Uint8Array;
6
+ accountB: Uint8Array;
7
+ pinA?: string;
8
+ pinB?: string;
9
+ }): {
10
+ a: Uint8Array<ArrayBufferLike>;
11
+ b: Uint8Array<ArrayBufferLike>;
12
+ };
13
+ export declare function createStatement(secret: SsSecret, payload: UnsignedStatement): Promise<import("@polkadot-api/sdk-statement").SignedStatement>;
@@ -0,0 +1,20 @@
1
+ import { getStatementSigner } from '@polkadot-api/sdk-statement';
2
+ import * as sr25519 from '@scure/sr25519';
3
+ import { getSsPub, khash, mergeBytes, stringToBytes } from './crypto.js';
4
+ export function createSession({ sharedSecret, accountA, accountB, pinA, pinB, }) {
5
+ const sessionPrefix = stringToBytes('session');
6
+ const pinSeparator = stringToBytes('/');
7
+ function makePin(pin) {
8
+ return pin ? mergeBytes(pinSeparator, stringToBytes(pin)) : pinSeparator;
9
+ }
10
+ const accountASessionParams = mergeBytes(sessionPrefix, accountA, accountB, makePin(pinA), makePin(pinB));
11
+ const accountBSessionParams = mergeBytes(sessionPrefix, accountB, accountA, makePin(pinB), makePin(pinA));
12
+ return {
13
+ a: khash(sharedSecret, accountASessionParams),
14
+ b: khash(sharedSecret, accountBSessionParams),
15
+ };
16
+ }
17
+ export function createStatement(secret, payload) {
18
+ const signer = getStatementSigner(getSsPub(secret), 'sr25519', data => sr25519.sign(secret, data));
19
+ return signer.sign(payload);
20
+ }
package/dist/papp.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Identity, IdentityAdapter } from './adapters/identity/types.js';
2
+ import type { StatementAdapter } from './adapters/statement/types.js';
3
+ import type { StorageAdapter } from './adapters/storage/types.js';
4
+ import type { SignInStatus } from './modules/signIn.js';
5
+ export type PappAdapter = {
6
+ auth: {
7
+ signIn(): Promise<Identity | null>;
8
+ abortSignIn(): void;
9
+ getCurrentUser(): Promise<Identity | null>;
10
+ getSignInStatus(): SignInStatus;
11
+ onSignInStatusChange(callback: (status: SignInStatus) => void): VoidFunction;
12
+ };
13
+ };
14
+ type Adapters = {
15
+ statements: StatementAdapter;
16
+ identities: IdentityAdapter;
17
+ storage: StorageAdapter;
18
+ };
19
+ type Params = {
20
+ appId: string;
21
+ metadata: string;
22
+ adapters?: Adapters;
23
+ };
24
+ export declare function createPappAdapter({ appId, metadata, adapters }: Params): PappAdapter;
25
+ export {};
package/dist/papp.js ADDED
@@ -0,0 +1,54 @@
1
+ import { getWsProvider } from '@polkadot-api/ws-provider';
2
+ import { createIdentityRpcAdapter } from './adapters/identity/rpc.js';
3
+ import { createPapiLazyClient } from './adapters/lazyClient/papi.js';
4
+ import { createPapiStatementAdapter } from './adapters/statement/rpc.js';
5
+ import { createMemoryAdapter } from './adapters/storage/memory.js';
6
+ import { SS_ENDPOINTS } from './constants.js';
7
+ import { createSignInFlow } from './modules/signIn.js';
8
+ export function createPappAdapter({ appId, metadata, adapters }) {
9
+ let statements;
10
+ let identities;
11
+ let storage;
12
+ if (adapters) {
13
+ statements = adapters.statements;
14
+ identities = adapters.identities;
15
+ storage = adapters.storage;
16
+ }
17
+ else {
18
+ const lazyPapiAdapter = createPapiLazyClient(getWsProvider(SS_ENDPOINTS));
19
+ storage = createMemoryAdapter();
20
+ identities = createIdentityRpcAdapter(lazyPapiAdapter, storage);
21
+ statements = createPapiStatementAdapter(lazyPapiAdapter);
22
+ }
23
+ const signInFlow = createSignInFlow({ appId, metadata, statements, storage });
24
+ const papp = {
25
+ auth: {
26
+ signIn() {
27
+ return signInFlow.signIn().then(result => {
28
+ if (result) {
29
+ return identities.getIdentity(result.pappAccountId);
30
+ }
31
+ return null;
32
+ });
33
+ },
34
+ getCurrentUser() {
35
+ return signInFlow.getSignedUser().then(result => {
36
+ if (result) {
37
+ return identities.getIdentity(result.pappAccountId);
38
+ }
39
+ return null;
40
+ });
41
+ },
42
+ getSignInStatus() {
43
+ return signInFlow.getSignInStatus();
44
+ },
45
+ onSignInStatusChange(callback) {
46
+ return signInFlow.onStatusChange(callback);
47
+ },
48
+ abortSignIn() {
49
+ return signInFlow.abortSignIn();
50
+ },
51
+ },
52
+ };
53
+ return papp;
54
+ }
@@ -0,0 +1,24 @@
1
+ type TransportError = 'decryptionFailed' | 'decodingFailed' | 'unknown';
2
+ export declare const ResponseCode: import("scale-ts").Codec<TransportError>;
3
+ export declare const RequestV1: import("scale-ts").Codec<{
4
+ requestId: string;
5
+ data: Uint8Array<ArrayBufferLike>;
6
+ }>;
7
+ export declare const ResponseV1: import("scale-ts").Codec<{
8
+ requestId: string;
9
+ responseCode: TransportError;
10
+ }>;
11
+ export declare const StatementData: import("scale-ts").Codec<{
12
+ tag: "requestV1";
13
+ value: {
14
+ requestId: string;
15
+ data: Uint8Array<ArrayBufferLike>;
16
+ };
17
+ } | {
18
+ tag: "responseV1";
19
+ value: {
20
+ requestId: string;
21
+ responseCode: TransportError;
22
+ };
23
+ }>;
24
+ export {};
@@ -0,0 +1,32 @@
1
+ import { Bytes, Enum, Struct, enhanceCodec, str, u8 } from 'scale-ts';
2
+ export const ResponseCode = enhanceCodec(u8, error => {
3
+ switch (error) {
4
+ case 'decryptionFailed':
5
+ return 1;
6
+ case 'decodingFailed':
7
+ return 2;
8
+ case 'unknown':
9
+ return -1;
10
+ }
11
+ }, code => {
12
+ switch (code) {
13
+ case 1:
14
+ return 'decryptionFailed';
15
+ case 2:
16
+ return 'decodingFailed';
17
+ default:
18
+ return 'unknown';
19
+ }
20
+ });
21
+ export const RequestV1 = Struct({
22
+ requestId: str,
23
+ data: Bytes(),
24
+ });
25
+ export const ResponseV1 = Struct({
26
+ requestId: str,
27
+ responseCode: ResponseCode,
28
+ });
29
+ export const StatementData = Enum({
30
+ requestV1: RequestV1,
31
+ responseV1: ResponseV1,
32
+ });
@@ -0,0 +1,6 @@
1
+ declare const __brand: unique symbol;
2
+ export type Branded<T, K extends string> = T & {
3
+ [__brand]: K;
4
+ };
5
+ export type SessionTopic = Branded<Uint8Array, 'SessionTopic'>;
6
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@novasamatech/host-papp",
3
+ "type": "module",
4
+ "version": "0.4.0-0",
5
+ "description": "Polkadot app integration",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/novasamatech/spektr-sdk.git"
10
+ },
11
+ "keywords": [
12
+ "polkadot"
13
+ ],
14
+ "main": "dist/index.js",
15
+ "exports": {
16
+ "./package.json": "./package.json",
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "dependencies": {
27
+ "@polkadot-api/utils": "0.2.0",
28
+ "@polkadot-api/sdk-statement": "0.2.0",
29
+ "@scure/sr25519": "0.3.0",
30
+ "@noble/curves": "2.0.1",
31
+ "@noble/hashes": "2.0.1",
32
+ "@noble/ciphers": "2.0.1",
33
+ "polkadot-api": "1.22.0",
34
+ "nanoevents": "9.1.0",
35
+ "scale-ts": "1.6.1"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }