@novasamatech/host-papp 0.5.0-1 → 0.5.0-11
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/.papi/descriptors/dist/common-types.d.ts +8667 -0
- package/.papi/descriptors/dist/common.d.ts +1 -0
- package/.papi/descriptors/dist/descriptors-UUEW32EL.mjs +27 -0
- package/.papi/descriptors/dist/descriptors.d.ts +1 -0
- package/.papi/descriptors/dist/index.d.ts +10 -0
- package/.papi/descriptors/dist/index.js +237 -0
- package/.papi/descriptors/dist/index.mjs +148 -0
- package/.papi/descriptors/dist/metadataTypes-E4AQJDJR.mjs +6 -0
- package/.papi/descriptors/dist/metadataTypes.d.ts +2 -0
- package/.papi/descriptors/dist/people_lite.d.ts +7757 -0
- package/.papi/descriptors/dist/people_lite_metadata-EIVHV27X.mjs +6 -0
- package/.papi/descriptors/dist/people_lite_metadata.d.ts +2 -0
- package/.papi/descriptors/package.json +24 -0
- package/.papi/metadata/people_lite.scale +0 -0
- package/.papi/polkadot-api.json +15 -0
- package/dist/adapters/identity/rpc.d.ts +6 -4
- package/dist/adapters/identity/rpc.js +96 -26
- package/dist/adapters/identity/types.d.ts +3 -1
- package/dist/adapters/lazyClient/papi.js +5 -0
- package/dist/adapters/lazyClient/types.d.ts +1 -0
- package/dist/adapters/statement/rpc.js +58 -10
- package/dist/adapters/statement/types.d.ts +6 -3
- package/dist/adapters/storage/localStorage.js +26 -4
- package/dist/adapters/storage/memory.js +14 -4
- package/dist/adapters/storage/types.d.ts +5 -2
- package/dist/adapters/storage/types.js +1 -1
- package/dist/components/auth/codec.d.ts +9 -0
- package/dist/components/auth/codec.js +10 -0
- package/dist/components/auth/codecs.d.ts +9 -0
- package/dist/components/auth/codecs.js +10 -0
- package/dist/components/auth/index.d.ts +36 -0
- package/dist/components/auth/index.js +150 -0
- package/dist/components/auth/types.d.ts +15 -0
- package/dist/components/auth/types.js +1 -0
- package/dist/components/session.d.ts +34 -0
- package/dist/components/session.js +54 -0
- package/dist/components/sso/index.d.ts +36 -0
- package/dist/components/sso/index.js +150 -0
- package/dist/components/sso/scale/handshake.d.ts +9 -0
- package/dist/components/sso/scale/handshake.js +10 -0
- package/dist/components/sso/types.d.ts +15 -0
- package/dist/components/sso/types.js +1 -0
- package/dist/components/transport.d.ts +27 -0
- package/dist/components/transport.js +57 -0
- package/dist/components/user/codec.d.ts +16 -0
- package/dist/components/user/codec.js +13 -0
- package/dist/components/user/index.d.ts +22 -0
- package/dist/components/user/index.js +58 -0
- package/dist/components/user/ssoMessageStream.d.ts +10 -0
- package/dist/components/user/ssoMessageStream.js +8 -0
- package/dist/components/user/ssoSession.d.ts +5 -0
- package/dist/components/user/ssoSession.js +5 -0
- package/dist/components/user/storage.d.ts +27 -0
- package/dist/components/user/storage.js +143 -0
- package/dist/components/user/types.d.ts +6 -0
- package/dist/components/user/types.js +1 -0
- package/dist/components/user/userSessionStorage.d.ts +20 -0
- package/dist/components/user/userSessionStorage.js +24 -0
- package/dist/components/user.d.ts +74 -0
- package/dist/components/user.js +188 -0
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +5 -1
- package/dist/crypto.d.ts +29 -0
- package/dist/crypto.js +86 -0
- package/dist/helpers/abortError.d.ts +4 -0
- package/dist/helpers/abortError.js +8 -0
- package/dist/helpers/callbackRaceResolver.d.ts +1 -0
- package/dist/helpers/callbackRaceResolver.js +17 -0
- package/dist/helpers/result.d.ts +12 -0
- package/dist/helpers/result.js +15 -0
- package/dist/helpers/result.spec.d.ts +1 -0
- package/dist/helpers/result.spec.js +23 -0
- package/dist/helpers/state.d.ts +16 -0
- package/dist/helpers/state.js +51 -0
- package/dist/helpers/utils.d.ts +2 -1
- package/dist/helpers/utils.js +11 -2
- package/dist/helpers/zipWith.d.ts +4 -0
- package/dist/helpers/zipWith.js +11 -0
- package/dist/identity/impl.d.ts +6 -0
- package/dist/identity/impl.js +68 -0
- package/dist/identity/rpcAdapter.d.ts +3 -0
- package/dist/identity/rpcAdapter.js +46 -0
- package/dist/identity/types.d.ts +21 -0
- package/dist/identity/types.js +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +2 -7
- package/dist/modules/crypto.d.ts +8 -9
- package/dist/modules/crypto.js +20 -42
- package/dist/modules/secretStorage.d.ts +13 -12
- package/dist/modules/secretStorage.js +34 -43
- package/dist/modules/session/helpers.d.ts +5 -0
- package/dist/modules/session/helpers.js +29 -0
- package/dist/modules/session/session.d.ts +12 -0
- package/dist/modules/session/session.js +50 -0
- package/dist/modules/session/types.d.ts +12 -0
- package/dist/modules/session/types.js +1 -0
- package/dist/modules/signIn.d.ts +32 -11
- package/dist/modules/signIn.js +97 -101
- package/dist/modules/state.d.ts +16 -0
- package/dist/modules/state.js +50 -0
- package/dist/modules/statementStore.d.ts +10 -11
- package/dist/modules/statementStore.js +16 -14
- package/dist/modules/statementTopic.d.ts +34 -0
- package/dist/modules/statementTopic.js +46 -0
- package/dist/modules/storageView.d.ts +25 -0
- package/dist/modules/storageView.js +51 -0
- package/dist/modules/syncStorage.d.ts +25 -0
- package/dist/modules/syncStorage.js +76 -0
- package/dist/modules/transport/codec.d.ts +24 -0
- package/dist/modules/transport/codec.js +36 -0
- package/dist/modules/transport/crypto.d.ts +2 -0
- package/dist/modules/transport/crypto.js +20 -0
- package/dist/modules/transport/transport.d.ts +42 -0
- package/dist/modules/transport/transport.js +66 -0
- package/dist/modules/user.d.ts +67 -0
- package/dist/modules/user.js +188 -0
- package/dist/modules/userManager.d.ts +15 -0
- package/dist/modules/userManager.js +105 -0
- package/dist/modules/userStorage.d.ts +19 -0
- package/dist/modules/userStorage.js +108 -0
- package/dist/modules/userStore.d.ts +15 -0
- package/dist/modules/userStore.js +105 -0
- package/dist/papp.d.ts +25 -13
- package/dist/papp.js +19 -50
- package/dist/sso/auth/attestationService.d.ts +18 -0
- package/dist/sso/auth/attestationService.js +171 -0
- package/dist/sso/auth/impl.d.ts +53 -0
- package/dist/sso/auth/impl.js +161 -0
- package/dist/sso/auth/scale/handshake.d.ts +9 -0
- package/dist/sso/auth/scale/handshake.js +10 -0
- package/dist/sso/auth/types.d.ts +17 -0
- package/dist/sso/auth/types.js +1 -0
- package/dist/sso/session/impl.d.ts +23 -0
- package/dist/sso/session/impl.js +57 -0
- package/dist/sso/session/scale/remoteMessage.d.ts +10 -0
- package/dist/sso/session/scale/remoteMessage.js +13 -0
- package/dist/sso/session/sessionManager.d.ts +23 -0
- package/dist/sso/session/sessionManager.js +58 -0
- package/dist/sso/session/ssoSession.d.ts +8 -0
- package/dist/sso/session/ssoSession.js +5 -0
- package/dist/sso/session/ssoSessionStorage.d.ts +21 -0
- package/dist/sso/session/ssoSessionStorage.js +20 -0
- package/dist/sso/session/types.d.ts +6 -0
- package/dist/sso/session/types.js +1 -0
- package/dist/sso/session/userSessionStorage.d.ts +21 -0
- package/dist/sso/session/userSessionStorage.js +20 -0
- package/dist/sso/sessionManager/attestationService.d.ts +5 -0
- package/dist/sso/sessionManager/attestationService.js +15 -0
- package/dist/sso/sessionManager/impl.d.ts +22 -0
- package/dist/sso/sessionManager/impl.js +71 -0
- package/dist/sso/sessionManager/repository/ssoSessionRepository.d.ts +22 -0
- package/dist/sso/sessionManager/repository/ssoSessionRepository.js +27 -0
- package/dist/sso/sessionManager/scale/hex.d.ts +1 -0
- package/dist/sso/sessionManager/scale/hex.js +3 -0
- package/dist/sso/sessionManager/scale/remoteMessage.d.ts +43 -0
- package/dist/sso/sessionManager/scale/remoteMessage.js +13 -0
- package/dist/sso/sessionManager/scale/signPayloadRequest.d.ts +19 -0
- package/dist/sso/sessionManager/scale/signPayloadRequest.js +19 -0
- package/dist/sso/sessionManager/scale/signPayloadResponse.d.ts +12 -0
- package/dist/sso/sessionManager/scale/signPayloadResponse.js +9 -0
- package/dist/sso/sessionManager/scale/signRequest.d.ts +19 -0
- package/dist/sso/sessionManager/scale/signRequest.js +19 -0
- package/dist/sso/sessionManager/scale/signResponse.d.ts +6 -0
- package/dist/sso/sessionManager/scale/signResponse.js +5 -0
- package/dist/sso/sessionManager/ssoSession.d.ts +23 -0
- package/dist/sso/sessionManager/ssoSession.js +69 -0
- package/dist/sso/sessionManager/ssoSessionProver.d.ts +4 -0
- package/dist/sso/sessionManager/ssoSessionProver.js +35 -0
- package/dist/sso/sessionManager/types.d.ts +6 -0
- package/dist/sso/sessionManager/types.js +1 -0
- package/dist/sso/sessionManager/userSession.d.ts +22 -0
- package/dist/sso/sessionManager/userSession.js +111 -0
- package/dist/sso/ssoSessionProver.d.ts +4 -0
- package/dist/sso/ssoSessionProver.js +35 -0
- package/dist/sso/ssoSessionRepository.d.ts +18 -0
- package/dist/sso/ssoSessionRepository.js +27 -0
- package/dist/sso/userSecretRepository.d.ts +17 -0
- package/dist/sso/userSecretRepository.js +45 -0
- package/dist/sso/userSessionRepository.d.ts +18 -0
- package/dist/sso/userSessionRepository.js +26 -0
- package/dist/structs.d.ts +10 -10
- package/dist/structs.js +17 -13
- package/dist/types.d.ts +1 -1
- package/package.json +14 -7
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { okAsync } from 'neverthrow';
|
|
2
|
+
import { callbackRaceResolver } from '../../helpers/callbackRaceResolver.js';
|
|
3
|
+
import { seq, seqAsync } from '../../helpers/result.js';
|
|
4
|
+
import { nonNullable } from '../../helpers/utils.js';
|
|
5
|
+
import { createSecretStorage } from '../../modules/secretStorage.js';
|
|
6
|
+
import { storageListView, storageView } from '../../modules/storageView.js';
|
|
7
|
+
export const createUserStorage = ({ appId, storage }) => {
|
|
8
|
+
const secretStorage = createSecretStorage(appId, storage);
|
|
9
|
+
const usersStorage = createSessionsStorage(storage);
|
|
10
|
+
const selectedUserStorage = createSelectedUserStorage(storage);
|
|
11
|
+
const store = {
|
|
12
|
+
sessions: {
|
|
13
|
+
read(accountId) {
|
|
14
|
+
const sessions = seqAsync(secretStorage.readSessionId(accountId, 'A'), secretStorage.readSessionId(accountId, 'B'));
|
|
15
|
+
return sessions.map(([sessionIdA, sessionIdB]) => {
|
|
16
|
+
if (nonNullable(sessionIdA) && nonNullable(sessionIdB)) {
|
|
17
|
+
return { accountId, sessionIdA, sessionIdB };
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
readSelectedUser() {
|
|
23
|
+
return store.accounts.readSelectedUser().andThen(selectedUser => {
|
|
24
|
+
if (selectedUser === null) {
|
|
25
|
+
return okAsync(null);
|
|
26
|
+
}
|
|
27
|
+
return store.sessions.read(selectedUser).andThen(user => {
|
|
28
|
+
if (user === null) {
|
|
29
|
+
return selectedUserStorage.clear().map(() => user);
|
|
30
|
+
}
|
|
31
|
+
return okAsync(user);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
subscribeSessions(callback) {
|
|
36
|
+
const resolver = callbackRaceResolver(callback, async (accounts) => {
|
|
37
|
+
if (accounts.length === 0) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return seq(...(await Promise.all(accounts.map(store.sessions.read))))
|
|
41
|
+
.map(sessions => sessions.filter(nonNullable))
|
|
42
|
+
.unwrapOr([]);
|
|
43
|
+
});
|
|
44
|
+
return store.accounts.subscribe(resolver);
|
|
45
|
+
},
|
|
46
|
+
create(user, secrets) {
|
|
47
|
+
return usersStorage
|
|
48
|
+
.add(user.accountId)
|
|
49
|
+
.andThen(() => seqAsync(secretStorage.writeSsSecret(user.accountId, secrets.ss), secretStorage.writeEncrSecret(user.accountId, secrets.encr), secretStorage.writeSharedSecret(user.accountId, secrets.sharedSecret), secretStorage.writeSessionId(user.accountId, 'A', user.sessionIdA), secretStorage.writeSessionId(user.accountId, 'B', user.sessionIdB), selectedUserStorage.write(user.accountId)).map(() => user));
|
|
50
|
+
},
|
|
51
|
+
remove(accountId) {
|
|
52
|
+
const op = seqAsync(secretStorage.clearSsSecret(accountId), secretStorage.clearEncrSecret(accountId), secretStorage.clearSharedSecret(accountId), secretStorage.clearSessionId(accountId, 'A'), secretStorage.clearSessionId(accountId, 'B'), usersStorage.remove(accountId), selectedUserStorage.read());
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
54
|
+
return op.andThen(([_1, _2, _3, _4, _5, users, selectedUser]) => {
|
|
55
|
+
if (selectedUser === accountId) {
|
|
56
|
+
const nextSelectedUser = users.at(0);
|
|
57
|
+
if (nextSelectedUser) {
|
|
58
|
+
return selectedUserStorage.write(nextSelectedUser).map(() => undefined);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return selectedUserStorage.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return okAsync(undefined);
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
readSecrets(accountId) {
|
|
68
|
+
const op = seqAsync(secretStorage.readSsSecret(accountId), secretStorage.readEncrSecret(accountId), secretStorage.readSharedSecret(accountId));
|
|
69
|
+
return op.map(([ss, encr, sharedSecret]) => {
|
|
70
|
+
if (nonNullable(ss) && nonNullable(encr) && nonNullable(sharedSecret)) {
|
|
71
|
+
return { ss, encr, sharedSecret };
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
accounts: {
|
|
78
|
+
read() {
|
|
79
|
+
return usersStorage.read();
|
|
80
|
+
},
|
|
81
|
+
subscribe(callback) {
|
|
82
|
+
return usersStorage.subscribe(callback);
|
|
83
|
+
},
|
|
84
|
+
readSelectedUser() {
|
|
85
|
+
return selectedUserStorage.read();
|
|
86
|
+
},
|
|
87
|
+
subscribeSelectedAccount(callback) {
|
|
88
|
+
return selectedUserStorage.subscribe(callback);
|
|
89
|
+
},
|
|
90
|
+
select(accountId) {
|
|
91
|
+
return selectedUserStorage.write(accountId).map(() => undefined);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
return store;
|
|
96
|
+
};
|
|
97
|
+
const createSessionsStorage = (storage) => {
|
|
98
|
+
const view = storageListView({
|
|
99
|
+
storage,
|
|
100
|
+
key: 'Users',
|
|
101
|
+
autosync: true,
|
|
102
|
+
initial: [],
|
|
103
|
+
from: x => JSON.parse(x),
|
|
104
|
+
to: x => JSON.stringify(x),
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
read() {
|
|
108
|
+
return view.read();
|
|
109
|
+
},
|
|
110
|
+
add(user) {
|
|
111
|
+
return view.mutate(users => {
|
|
112
|
+
if (users.some(x => x === user)) {
|
|
113
|
+
throw new Error(`User ${user} already exists.`);
|
|
114
|
+
}
|
|
115
|
+
return users.concat(user);
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
remove(user) {
|
|
119
|
+
return view.mutate(users => {
|
|
120
|
+
const newUsers = users.filter(x => x !== user);
|
|
121
|
+
if (newUsers.length !== users.length) {
|
|
122
|
+
return newUsers;
|
|
123
|
+
}
|
|
124
|
+
return users;
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
subscribe(callback) {
|
|
128
|
+
return view.subscribe(v => {
|
|
129
|
+
callback(v ?? []);
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const createSelectedUserStorage = (storage) => {
|
|
135
|
+
return storageView({
|
|
136
|
+
storage,
|
|
137
|
+
key: 'SelectedUser',
|
|
138
|
+
autosync: true,
|
|
139
|
+
initial: null,
|
|
140
|
+
from: x => x,
|
|
141
|
+
to: x => x,
|
|
142
|
+
});
|
|
143
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StorageAdapter } from '../../adapters/storage/types.js';
|
|
2
|
+
import type { Account } from '../../modules/session/types.js';
|
|
3
|
+
export type UserSessionStorage = ReturnType<typeof createUserSessionStorage>;
|
|
4
|
+
export type UserSession = {
|
|
5
|
+
id: string;
|
|
6
|
+
host: Account;
|
|
7
|
+
peer: Account;
|
|
8
|
+
};
|
|
9
|
+
type Params = {
|
|
10
|
+
storage: StorageAdapter;
|
|
11
|
+
};
|
|
12
|
+
export declare const createUserSessionStorage: ({ storage }: Params) => {
|
|
13
|
+
add(value: UserSession): import("neverthrow").ResultAsync<UserSession, Error>;
|
|
14
|
+
mutate(fn: (value: UserSession[]) => UserSession[]): import("neverthrow").ResultAsync<UserSession[], Error>;
|
|
15
|
+
read(): any;
|
|
16
|
+
write(value: UserSession[]): any;
|
|
17
|
+
clear(): any;
|
|
18
|
+
subscribe(fn: (value: UserSession[]) => void): () => void;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { fromHex, toHex } from '@polkadot-api/utils';
|
|
2
|
+
import { Bytes, Option, Struct, Vector, str } from 'scale-ts';
|
|
3
|
+
import { storageListView } from '../../modules/storageView.js';
|
|
4
|
+
const accountCodec = Struct({
|
|
5
|
+
accountId: Bytes(),
|
|
6
|
+
publicKey: Bytes(),
|
|
7
|
+
pin: Option(str),
|
|
8
|
+
});
|
|
9
|
+
const userSessionCodec = Struct({
|
|
10
|
+
id: str,
|
|
11
|
+
host: accountCodec,
|
|
12
|
+
peer: accountCodec,
|
|
13
|
+
});
|
|
14
|
+
const userSessionsCodec = Vector(userSessionCodec);
|
|
15
|
+
export const createUserSessionStorage = ({ storage }) => {
|
|
16
|
+
return storageListView({
|
|
17
|
+
storage,
|
|
18
|
+
key: 'Sessions',
|
|
19
|
+
autosync: true,
|
|
20
|
+
initial: [],
|
|
21
|
+
from: x => userSessionsCodec.dec(fromHex(x)),
|
|
22
|
+
to: x => toHex(userSessionsCodec.enc(x)),
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { StatementAdapter } from '../adapters/statement/types.js';
|
|
2
|
+
import type { StorageAdapter } from '../adapters/storage/types.js';
|
|
3
|
+
import type { Result } from '../helpers/result.js';
|
|
4
|
+
import type { EncrPublicKey, SsPublicKey } from '../modules/crypto.js';
|
|
5
|
+
import type { UserSession } from '../modules/userStorage.js';
|
|
6
|
+
export declare const HandshakeData: import("scale-ts").Codec<{
|
|
7
|
+
tag: "V1";
|
|
8
|
+
value: [SsPublicKey, EncrPublicKey, string];
|
|
9
|
+
}>;
|
|
10
|
+
export declare const HandshakeResponsePayload: import("scale-ts").Codec<{
|
|
11
|
+
tag: "V1";
|
|
12
|
+
value: [Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>];
|
|
13
|
+
}>;
|
|
14
|
+
export declare const HandshakeResponseSensitiveData: import("scale-ts").Codec<[Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>]>;
|
|
15
|
+
export type AuthentificationStatus = {
|
|
16
|
+
step: 'none';
|
|
17
|
+
} | {
|
|
18
|
+
step: 'initial';
|
|
19
|
+
} | {
|
|
20
|
+
step: 'pairing';
|
|
21
|
+
payload: string;
|
|
22
|
+
} | {
|
|
23
|
+
step: 'error';
|
|
24
|
+
message: string;
|
|
25
|
+
} | {
|
|
26
|
+
step: 'finished';
|
|
27
|
+
user: UserSession;
|
|
28
|
+
};
|
|
29
|
+
type Params = {
|
|
30
|
+
/**
|
|
31
|
+
* Host app Id.
|
|
32
|
+
* CAUTION! This value should be stable.
|
|
33
|
+
*/
|
|
34
|
+
appId: string;
|
|
35
|
+
/**
|
|
36
|
+
* URL for additional metadata that will be displayed during pairing process.
|
|
37
|
+
* Content of provided json shound be
|
|
38
|
+
* ```ts
|
|
39
|
+
* interface Metadata {
|
|
40
|
+
* name: string;
|
|
41
|
+
* icon: string; // url for icon. Icon should be a rasterized image with min size 256x256 px.
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
metadata: string;
|
|
46
|
+
statements: StatementAdapter;
|
|
47
|
+
storage: StorageAdapter;
|
|
48
|
+
};
|
|
49
|
+
export declare function createUserComponent({ appId, metadata, statements, storage }: Params): {
|
|
50
|
+
authStatus: {
|
|
51
|
+
touched(): boolean;
|
|
52
|
+
read(): AuthentificationStatus;
|
|
53
|
+
write(value: AuthentificationStatus): AuthentificationStatus;
|
|
54
|
+
reset(): void;
|
|
55
|
+
subscribe(fn: (value: AuthentificationStatus) => void): () => void;
|
|
56
|
+
onFirstSubscribe(callback: VoidFunction): import("nanoevents").Unsubscribe;
|
|
57
|
+
onLastUnsubscribe(callback: VoidFunction): import("nanoevents").Unsubscribe;
|
|
58
|
+
};
|
|
59
|
+
storage: {
|
|
60
|
+
sessions: {
|
|
61
|
+
read(accountId: string): Promise<Result<UserSession | null>>;
|
|
62
|
+
readSelectedUser(): Promise<Result<UserSession | null>>;
|
|
63
|
+
create(user: UserSession): Promise<Result<UserSession>>;
|
|
64
|
+
remove(accountId: string): Promise<Result<void, Error>>;
|
|
65
|
+
};
|
|
66
|
+
accounts: {
|
|
67
|
+
read(): Promise<Result<string[], Error>>;
|
|
68
|
+
select(accountId: string): Promise<Result<string | null, Error> | Result<null, Error>>;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
authenticate(): Promise<Result<UserSession | null>>;
|
|
72
|
+
abortAuthentication(): void;
|
|
73
|
+
};
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { toHex } from '@polkadot-api/utils';
|
|
2
|
+
import { Bytes, Enum, Tuple, str } from 'scale-ts';
|
|
3
|
+
import { err, fromPromise, ok, seq } from '../helpers/result.js';
|
|
4
|
+
import { isAbortError, toError } from '../helpers/utils.js';
|
|
5
|
+
import { ENCR_SECRET_SEED_SIZE, EncrPubKey, SS_SECRET_SEED_SIZE, SsPubKey, createEncrSecret, createRandomSeed, createSharedSecret, createSsSecret, createSymmetricKey, decrypt, getEncrPub, getSsPub, khash, mergeBytes, stringToBytes, } from '../modules/crypto.js';
|
|
6
|
+
import { createSecretStorage } from '../modules/secretStorage.js';
|
|
7
|
+
import { createSession } from '../modules/statementStore.js';
|
|
8
|
+
import { createSyncStorage } from '../modules/syncStorage.js';
|
|
9
|
+
import { createUserStorage } from '../modules/userStorage.js';
|
|
10
|
+
// codecs
|
|
11
|
+
export const HandshakeData = Enum({
|
|
12
|
+
V1: Tuple(SsPubKey, EncrPubKey, str),
|
|
13
|
+
});
|
|
14
|
+
export const HandshakeResponsePayload = Enum({
|
|
15
|
+
// [encrypted, tmp_key]
|
|
16
|
+
V1: Tuple(Bytes(), Bytes(65)),
|
|
17
|
+
});
|
|
18
|
+
export const HandshakeResponseSensitiveData = Tuple(Bytes(65), Bytes(32));
|
|
19
|
+
export function createUserComponent({ appId, metadata, statements, storage }) {
|
|
20
|
+
const userStorage = createUserStorage(appId, storage);
|
|
21
|
+
const secretStorage = createSecretStorage(appId, storage);
|
|
22
|
+
const authStatus = createSyncStorage({ step: 'none' });
|
|
23
|
+
let authPromise = null;
|
|
24
|
+
let abort = null;
|
|
25
|
+
async function handshake(signal) {
|
|
26
|
+
authStatus.write({ step: 'initial' });
|
|
27
|
+
const secrets = await getSecretKeys(appId, secretStorage);
|
|
28
|
+
return secrets.andThenPromise(async ({ ssPublicKey, encrPublicKey, encrSecret }) => {
|
|
29
|
+
const handshakeTopic = createHandshakeTopic({ encrPublicKey, ssPublicKey });
|
|
30
|
+
const handshakePayload = createHandshakePayloadV1({ ssPublicKey, encrPublicKey, metadata });
|
|
31
|
+
authStatus.write({ step: 'pairing', payload: createDeeplink(handshakePayload) });
|
|
32
|
+
const statementStoreResponse = fromPromise(waitForStatements(statements, handshakeTopic, signal, (statements, resolve) => {
|
|
33
|
+
for (const statement of [...statements].reverse()) {
|
|
34
|
+
if (!statement.data)
|
|
35
|
+
continue;
|
|
36
|
+
const { sessionTopic, accountId } = retrieveSessionTopic({
|
|
37
|
+
payload: statement.data.asBytes(),
|
|
38
|
+
encrSecret,
|
|
39
|
+
ssPublicKey,
|
|
40
|
+
});
|
|
41
|
+
resolve({ sessionTopic, accountId: toHex(accountId) });
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}), toError);
|
|
45
|
+
return statementStoreResponse
|
|
46
|
+
.then(x => x.andThenPromise(userStorage.sessions.create))
|
|
47
|
+
.then(async (result) => result
|
|
48
|
+
.map(user => {
|
|
49
|
+
authStatus.write({ step: 'finished', user });
|
|
50
|
+
return user;
|
|
51
|
+
})
|
|
52
|
+
.orElse(e => {
|
|
53
|
+
const error = toError(e);
|
|
54
|
+
if (isAbortError(error)) {
|
|
55
|
+
authStatus.write({ step: 'none' });
|
|
56
|
+
return ok(null);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
authStatus.write({ step: 'error', message: error.message });
|
|
60
|
+
return err(error);
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const userModule = {
|
|
66
|
+
authStatus,
|
|
67
|
+
storage: userStorage,
|
|
68
|
+
async authenticate() {
|
|
69
|
+
if (authPromise) {
|
|
70
|
+
return authPromise;
|
|
71
|
+
}
|
|
72
|
+
abort = new AbortController();
|
|
73
|
+
authPromise = handshake(abort.signal);
|
|
74
|
+
return authPromise;
|
|
75
|
+
},
|
|
76
|
+
abortAuthentication() {
|
|
77
|
+
if (abort) {
|
|
78
|
+
authPromise = null;
|
|
79
|
+
authStatus.reset();
|
|
80
|
+
abort.abort();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return userModule;
|
|
85
|
+
}
|
|
86
|
+
function createHandshakeTopic({ encrPublicKey, ssPublicKey, }) {
|
|
87
|
+
return khash(ssPublicKey, mergeBytes(encrPublicKey, stringToBytes('topic')));
|
|
88
|
+
}
|
|
89
|
+
function createHandshakePayloadV1({ encrPublicKey, ssPublicKey, metadata, }) {
|
|
90
|
+
return HandshakeData.enc({
|
|
91
|
+
tag: 'V1',
|
|
92
|
+
value: [ssPublicKey, encrPublicKey, metadata],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function parseHandshakePayload(payload) {
|
|
96
|
+
const decoded = HandshakeResponsePayload.dec(payload);
|
|
97
|
+
switch (decoded.tag) {
|
|
98
|
+
case 'V1':
|
|
99
|
+
return {
|
|
100
|
+
encrypted: decoded.value[0],
|
|
101
|
+
tmpKey: decoded.value[1],
|
|
102
|
+
};
|
|
103
|
+
default:
|
|
104
|
+
throw new Error('Unsupported handshake payload version');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function retrieveSessionTopic({ payload, encrSecret, ssPublicKey, }) {
|
|
108
|
+
const { encrypted, tmpKey } = parseHandshakePayload(payload);
|
|
109
|
+
const symmetricKey = createSymmetricKey(createSharedSecret(encrSecret, tmpKey));
|
|
110
|
+
const decrypted = decrypt(symmetricKey, encrypted);
|
|
111
|
+
const [pappEncrPublicKey, userPublicKey] = HandshakeResponseSensitiveData.dec(decrypted);
|
|
112
|
+
const sharedSecret = createSharedSecret(encrSecret, pappEncrPublicKey);
|
|
113
|
+
const session = createSession({
|
|
114
|
+
sharedSecret: sharedSecret,
|
|
115
|
+
accountA: ssPublicKey,
|
|
116
|
+
accountB: pappEncrPublicKey,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
accountId: userPublicKey,
|
|
120
|
+
sessionTopic: session.a,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function getSsKeys(appId, secretStorage) {
|
|
124
|
+
return (await secretStorage.readSsSecret())
|
|
125
|
+
.andThenPromise(async (ssSecret) => {
|
|
126
|
+
if (ssSecret) {
|
|
127
|
+
return ok(ssSecret);
|
|
128
|
+
}
|
|
129
|
+
const seed = createRandomSeed(appId, SS_SECRET_SEED_SIZE);
|
|
130
|
+
const newSsSecret = createSsSecret(seed);
|
|
131
|
+
const write = await secretStorage.writeSsSecret(newSsSecret);
|
|
132
|
+
return write.map(() => newSsSecret);
|
|
133
|
+
})
|
|
134
|
+
.then(x => x.map(ssSecret => ({
|
|
135
|
+
ssSecret: ssSecret,
|
|
136
|
+
ssPublicKey: getSsPub(ssSecret),
|
|
137
|
+
})));
|
|
138
|
+
}
|
|
139
|
+
async function getEncrKeys(appId, secretStorage) {
|
|
140
|
+
return (await secretStorage.readEncrSecret())
|
|
141
|
+
.andThenPromise(async (encrSecret) => {
|
|
142
|
+
if (encrSecret) {
|
|
143
|
+
return ok(encrSecret);
|
|
144
|
+
}
|
|
145
|
+
const seed = createRandomSeed(appId, ENCR_SECRET_SEED_SIZE);
|
|
146
|
+
const newEncrSecret = createEncrSecret(seed);
|
|
147
|
+
const write = await secretStorage.writeEncrSecret(newEncrSecret);
|
|
148
|
+
return write.map(() => newEncrSecret);
|
|
149
|
+
})
|
|
150
|
+
.then(x => x.map(encrSecret => ({
|
|
151
|
+
encrSecret,
|
|
152
|
+
encrPublicKey: getEncrPub(encrSecret),
|
|
153
|
+
})));
|
|
154
|
+
}
|
|
155
|
+
async function getSecretKeys(appId, secretStorage) {
|
|
156
|
+
return seq(await getSsKeys(appId, secretStorage), await getEncrKeys(appId, secretStorage)).map(([ss, encr]) => ({
|
|
157
|
+
...ss,
|
|
158
|
+
...encr,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
function createDeeplink(payload) {
|
|
162
|
+
return `polkadotapp://pair?handshake=${toHex(payload)}`;
|
|
163
|
+
}
|
|
164
|
+
function waitForStatements(transport, topic, abortSignal, callback) {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const unsubscribe = transport.subscribeStatements([topic], statements => {
|
|
167
|
+
if (abortSignal?.aborted) {
|
|
168
|
+
unsubscribe();
|
|
169
|
+
try {
|
|
170
|
+
abortSignal.throwIfAborted();
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
reject(e);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
callback(statements, value => {
|
|
178
|
+
unsubscribe();
|
|
179
|
+
resolve(value);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
unsubscribe();
|
|
184
|
+
reject(e);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare const
|
|
1
|
+
export declare const SS_UNSTABLE_STAGE_ENDPOINTS: string[];
|
|
2
|
+
export declare const SS_STABLE_STAGE_ENDPOINTS: string[];
|
package/dist/constants.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const SS_UNSTABLE_STAGE_ENDPOINTS = ['wss://pop-testnet.parity-lab.parity.io:443/9910'];
|
|
2
|
+
export const SS_STABLE_STAGE_ENDPOINTS = [
|
|
3
|
+
'wss://pop3-testnet.parity-lab.parity.io:443/7911',
|
|
4
|
+
'wss://pop3-testnet.parity-lab.parity.io:443/7912',
|
|
5
|
+
];
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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 SharedSecret = Branded<Uint8Array, 'SharedSecret'>;
|
|
8
|
+
export type SharedSession = Branded<Uint8Array, 'SharedSession'>;
|
|
9
|
+
export declare function BrandedBytesCodec<T extends Uint8Array>(length?: number): Codec<T>;
|
|
10
|
+
export declare const SsPubKey: Codec<SsPublicKey>;
|
|
11
|
+
export declare const EncrPubKey: Codec<EncrPublicKey>;
|
|
12
|
+
export declare function stringToBytes(str: string): Uint8Array<ArrayBuffer>;
|
|
13
|
+
export declare function bytesToString(bytes: Uint8Array): string;
|
|
14
|
+
export declare function createSsSecret(mnemonic: string): SsSecret;
|
|
15
|
+
export declare function createSsDerivation(secret: SsSecret, derivation: string): SsSecret;
|
|
16
|
+
export declare function getSsPub(secret: SsSecret): SsPublicKey;
|
|
17
|
+
export declare function signWithSsSecret(secret: SsSecret, message: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
18
|
+
export declare function verifyWithSsSecret(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
|
|
19
|
+
export type DerivedSr25519Account = {
|
|
20
|
+
secret: SsSecret;
|
|
21
|
+
publicKey: SsPublicKey;
|
|
22
|
+
entropy: Uint8Array;
|
|
23
|
+
sign(message: Uint8Array): Uint8Array;
|
|
24
|
+
verify(message: Uint8Array, signature: Uint8Array): boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function deriveSr25519Account(mnemonic: string, derivation: string): DerivedSr25519Account;
|
|
27
|
+
export declare function createEncrSecret(mnemonic: string): EncrSecret;
|
|
28
|
+
export declare function getEncrPub(secret: EncrSecret): EncrPublicKey;
|
|
29
|
+
export declare function createSharedSecret(secret: EncrSecret, publicKey: Uint8Array): SharedSecret;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { p256 } from '@noble/curves/nist.js';
|
|
2
|
+
import { mnemonicToEntropy, mnemonicToMiniSecret } from '@polkadot-labs/hdkd-helpers';
|
|
3
|
+
import { HDKD as sr25519HDKD, getPublicKey as sr25519GetPublicKey, secretFromSeed as sr25519SecretFromSeed, sign as sr25519Sign, verify as sr25519Verify, } from '@scure/sr25519';
|
|
4
|
+
import { Bytes, str } from 'scale-ts';
|
|
5
|
+
// schemas
|
|
6
|
+
export function BrandedBytesCodec(length) {
|
|
7
|
+
return Bytes(length);
|
|
8
|
+
}
|
|
9
|
+
export const SsPubKey = BrandedBytesCodec(32);
|
|
10
|
+
export const EncrPubKey = BrandedBytesCodec(65);
|
|
11
|
+
// helpers
|
|
12
|
+
const textEncoder = new TextEncoder();
|
|
13
|
+
const textDecoder = new TextDecoder();
|
|
14
|
+
export function stringToBytes(str) {
|
|
15
|
+
return textEncoder.encode(str);
|
|
16
|
+
}
|
|
17
|
+
export function bytesToString(bytes) {
|
|
18
|
+
return textDecoder.decode(bytes);
|
|
19
|
+
}
|
|
20
|
+
function parseDerivations(derivationsStr) {
|
|
21
|
+
const DERIVATION_RE = /(\/{1,2})([^/]+)/g;
|
|
22
|
+
const derivations = [];
|
|
23
|
+
for (const [, type, code] of derivationsStr.matchAll(DERIVATION_RE)) {
|
|
24
|
+
if (code) {
|
|
25
|
+
derivations.push([type === '//' ? 'hard' : 'soft', code]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return derivations;
|
|
29
|
+
}
|
|
30
|
+
function createChainCode(derivation) {
|
|
31
|
+
const chainCode = new Uint8Array(32);
|
|
32
|
+
chainCode.set(str.enc(derivation));
|
|
33
|
+
return chainCode;
|
|
34
|
+
}
|
|
35
|
+
// statement store key pair
|
|
36
|
+
export function createSsSecret(mnemonic) {
|
|
37
|
+
const miniSecret = mnemonicToMiniSecret(mnemonic);
|
|
38
|
+
return sr25519SecretFromSeed(miniSecret);
|
|
39
|
+
}
|
|
40
|
+
export function createSsDerivation(secret, derivation) {
|
|
41
|
+
const derivations = parseDerivations(derivation);
|
|
42
|
+
return derivations.reduce((secret, [type, derivation]) => {
|
|
43
|
+
const chainCode = createChainCode(derivation);
|
|
44
|
+
switch (type) {
|
|
45
|
+
case 'hard':
|
|
46
|
+
return sr25519HDKD.secretHard(secret, chainCode);
|
|
47
|
+
case 'soft':
|
|
48
|
+
return sr25519HDKD.secretSoft(secret, chainCode);
|
|
49
|
+
}
|
|
50
|
+
}, secret);
|
|
51
|
+
}
|
|
52
|
+
export function getSsPub(secret) {
|
|
53
|
+
return sr25519GetPublicKey(secret);
|
|
54
|
+
}
|
|
55
|
+
export function signWithSsSecret(secret, message) {
|
|
56
|
+
return sr25519Sign(secret, message);
|
|
57
|
+
}
|
|
58
|
+
export function verifyWithSsSecret(message, signature, publicKey) {
|
|
59
|
+
return sr25519Verify(message, signature, publicKey);
|
|
60
|
+
}
|
|
61
|
+
export function deriveSr25519Account(mnemonic, derivation) {
|
|
62
|
+
const secret = createSsDerivation(createSsSecret(mnemonic), derivation);
|
|
63
|
+
const publicKey = getSsPub(secret);
|
|
64
|
+
return {
|
|
65
|
+
secret,
|
|
66
|
+
publicKey,
|
|
67
|
+
entropy: mnemonicToEntropy(mnemonic),
|
|
68
|
+
sign: message => signWithSsSecret(secret, message),
|
|
69
|
+
verify: (message, signature) => verifyWithSsSecret(message, signature, publicKey),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// encryption key pair
|
|
73
|
+
export function createEncrSecret(mnemonic) {
|
|
74
|
+
const miniSecret = mnemonicToMiniSecret(mnemonic);
|
|
75
|
+
const seed = new Uint8Array(48);
|
|
76
|
+
seed.set(miniSecret);
|
|
77
|
+
const { secretKey } = p256.keygen(seed);
|
|
78
|
+
return secretKey;
|
|
79
|
+
}
|
|
80
|
+
export function getEncrPub(secret) {
|
|
81
|
+
return p256.getPublicKey(secret, false);
|
|
82
|
+
}
|
|
83
|
+
export function createSharedSecret(secret, publicKey) {
|
|
84
|
+
// slicing first byte: @noble/curves adds y offset at the start
|
|
85
|
+
return p256.getSharedSecret(secret, publicKey).slice(1, 33);
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function callbackRaceResolver<T, R>(callback: (value: R) => unknown, preprocess: (value: T) => PromiseLike<R>): (value: T) => unknown;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function callbackRaceResolver(callback, preprocess) {
|
|
2
|
+
let abort = new AbortController();
|
|
3
|
+
return async (value) => {
|
|
4
|
+
abort.abort();
|
|
5
|
+
abort = new AbortController();
|
|
6
|
+
try {
|
|
7
|
+
const result = await preprocess(value);
|
|
8
|
+
if (abort.signal.aborted) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
callback(result);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* empty */
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Result, ResultAsync } from 'neverthrow';
|
|
2
|
+
type InferOk<R> = R extends Result<infer Ok, unknown> ? Ok : never;
|
|
3
|
+
type InferOkAsync<R> = R extends ResultAsync<infer Ok, unknown> ? Ok : never;
|
|
4
|
+
type InferErr<R> = R extends Result<unknown, infer Err> ? Err : never;
|
|
5
|
+
type InferErrAsync<R> = R extends ResultAsync<unknown, infer Err> ? Err : never;
|
|
6
|
+
type InferOks<Results> = Results extends [infer Head, ...infer Tail] ? [InferOk<Head>, ...InferOks<Tail>] : Results extends Result<unknown, unknown>[] ? InferOk<Results[number]>[] : [];
|
|
7
|
+
type InferOksAsync<Results> = Results extends [infer Head, ...infer Tail] ? [InferOkAsync<Head>, ...InferOksAsync<Tail>] : Results extends ResultAsync<unknown, unknown>[] ? InferOkAsync<Results[number]>[] : [];
|
|
8
|
+
type SeqResults<Results extends Result<unknown, unknown>[]> = Result<InferOks<Results>, InferErr<Results[number]> | Error>;
|
|
9
|
+
type SeqResultsAsync<Results extends ResultAsync<unknown, unknown>[]> = ResultAsync<InferOksAsync<Results>, InferErrAsync<Results[number]> | Error>;
|
|
10
|
+
export declare function seq<const Results extends Result<unknown, unknown>[]>(...result: Results): SeqResults<Results>;
|
|
11
|
+
export declare function seqAsync<const Results extends ResultAsync<unknown, unknown>[]>(...result: Results): SeqResultsAsync<Results>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { err, errAsync } from 'neverthrow';
|
|
2
|
+
export function seq(...result) {
|
|
3
|
+
const [head, ...tail] = result;
|
|
4
|
+
if (head === undefined) {
|
|
5
|
+
return err(new Error('Seq is empty'));
|
|
6
|
+
}
|
|
7
|
+
return tail.reduce((a, r) => a.andThen(av => r.map(rv => [...av, rv])), head.map(r => [r]));
|
|
8
|
+
}
|
|
9
|
+
export function seqAsync(...result) {
|
|
10
|
+
const [head, ...tail] = result;
|
|
11
|
+
if (head === undefined) {
|
|
12
|
+
return errAsync(new Error('Seq is empty'));
|
|
13
|
+
}
|
|
14
|
+
return tail.reduce((a, r) => a.andThen(av => r.map(rv => [...av, rv])), head.map(r => [r]));
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|