@novasamatech/host-papp 0.7.9 → 0.8.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.
- package/README.md +170 -1
- package/dist/debugTypes.d.ts +3 -5
- package/dist/identity/rpcAdapter.js +12 -5
- package/dist/index.d.ts +2 -1
- package/dist/papp.d.ts +21 -16
- package/dist/papp.js +6 -2
- package/dist/sso/auth/impl.d.ts +25 -7
- package/dist/sso/auth/impl.js +126 -188
- package/dist/sso/auth/scale/handshakeV2.d.ts +260 -0
- package/dist/sso/auth/scale/handshakeV2.js +176 -0
- package/dist/sso/auth/v2/envelope.d.ts +18 -0
- package/dist/sso/auth/v2/envelope.js +23 -0
- package/dist/sso/auth/v2/proposal.d.ts +23 -0
- package/dist/sso/auth/v2/proposal.js +43 -0
- package/dist/sso/auth/v2/service.d.ts +64 -0
- package/dist/sso/auth/v2/service.js +184 -0
- package/dist/sso/auth/v2/state.d.ts +90 -0
- package/dist/sso/auth/v2/state.js +65 -0
- package/dist/sso/auth/v2/topic.d.ts +17 -0
- package/dist/sso/auth/v2/topic.js +21 -0
- package/dist/sso/deviceIdentityStore.d.ts +14 -0
- package/dist/sso/deviceIdentityStore.js +76 -0
- package/dist/sso/sessionManager/userSession.js +1 -4
- package/dist/sso/userSecretRepository.d.ts +1 -0
- package/dist/sso/userSecretRepository.js +16 -1
- package/dist/sso/userSessionRepository.d.ts +29 -1
- package/dist/sso/userSessionRepository.js +22 -5
- package/package.json +9 -6
package/dist/sso/auth/impl.js
CHANGED
|
@@ -1,230 +1,168 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { generateMnemonic } from '@polkadot-labs/hdkd-helpers';
|
|
4
|
-
import { Result, ResultAsync, err, fromPromise, fromThrowable, ok } from 'neverthrow';
|
|
5
|
-
import { mergeUint8, toHex } from 'polkadot-api/utils';
|
|
6
|
-
import { createEncrSecret, createSharedSecret, deriveSr25519Account, getEncrPub, stringToBytes } from '../../crypto.js';
|
|
1
|
+
import { createAccountId, createLocalSessionAccount, createRemoteSessionAccount } from '@novasamatech/statement-store';
|
|
2
|
+
import { ResultAsync, errAsync, okAsync } from 'neverthrow';
|
|
7
3
|
import { createFlowId, emitHostPappDebugMessage } from '../../debugBus.js';
|
|
8
4
|
import { AbortError } from '../../helpers/abortError.js';
|
|
9
5
|
import { createState, readonly } from '../../helpers/state.js';
|
|
10
6
|
import { toError } from '../../helpers/utils.js';
|
|
11
7
|
import { createStoredUserSession } from '../userSessionRepository.js';
|
|
12
|
-
import {
|
|
13
|
-
export function createAuth({
|
|
8
|
+
import { startPairingV2 } from './v2/service.js';
|
|
9
|
+
export function createAuth({ hostMetadata, deviceIdentity, deviceIdentityStore, statementStore, ssoSessionRepository, userSecretRepository, onAuthSuccess, }) {
|
|
14
10
|
const pairingStatus = createState({ step: 'none' });
|
|
15
11
|
let authResult = null;
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
const localAccount = createLocalSessionAccount(createAccountId(account.publicKey));
|
|
19
|
-
pairingStatus.write({ step: 'initial' });
|
|
20
|
-
const encrKeys = createEncrKeys(account.entropy);
|
|
21
|
-
const handshakePayload = encrKeys.andThen(({ publicKey }) => createHandshakePayloadV1({
|
|
22
|
-
ssPublicKey: account.publicKey,
|
|
23
|
-
encrPublicKey: publicKey,
|
|
24
|
-
metadata,
|
|
25
|
-
hostMetadata,
|
|
26
|
-
}));
|
|
27
|
-
const handshakeTopic = encrKeys.andThen(({ publicKey }) => createHandshakeTopic(localAccount, publicKey));
|
|
28
|
-
const dataPrepared = Result.combine([handshakePayload, handshakeTopic, encrKeys]).andTee(([payload]) => {
|
|
29
|
-
const deeplink = createDeeplink(payload);
|
|
30
|
-
pairingStatus.write({ step: 'pairing', payload: deeplink });
|
|
31
|
-
emitHostPappDebugMessage({
|
|
32
|
-
layer: 'sso',
|
|
33
|
-
event: 'deeplink_generated',
|
|
34
|
-
flowId,
|
|
35
|
-
timestamp: Date.now(),
|
|
36
|
-
payload: { deeplink },
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
return dataPrepared
|
|
40
|
-
.asyncAndThen(([, handshakeTopic, encrKeys]) => {
|
|
41
|
-
emitHostPappDebugMessage({
|
|
42
|
-
layer: 'sso',
|
|
43
|
-
event: 'awaiting_response',
|
|
44
|
-
flowId,
|
|
45
|
-
timestamp: Date.now(),
|
|
46
|
-
payload: { topic: toHex(handshakeTopic) },
|
|
47
|
-
});
|
|
48
|
-
const pappResponse = waitForStatements(callback => statementStore.subscribeStatements({ matchAll: [handshakeTopic] }, page => callback(page.statements)), signal, (statements, resolve) => {
|
|
49
|
-
for (const statement of statements) {
|
|
50
|
-
if (!statement.data)
|
|
51
|
-
continue;
|
|
52
|
-
const session = retrieveSession({
|
|
53
|
-
localAccount,
|
|
54
|
-
encrSecret: encrKeys.secret,
|
|
55
|
-
payload: statement.data,
|
|
56
|
-
}).unwrapOr(null);
|
|
57
|
-
if (session) {
|
|
58
|
-
emitHostPappDebugMessage({
|
|
59
|
-
layer: 'sso',
|
|
60
|
-
event: 'response_received',
|
|
61
|
-
flowId,
|
|
62
|
-
timestamp: Date.now(),
|
|
63
|
-
payload: { sessionId: session.id },
|
|
64
|
-
});
|
|
65
|
-
resolve(session);
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
return pappResponse.map(session => ({
|
|
71
|
-
session,
|
|
72
|
-
secretsPayload: {
|
|
73
|
-
id: session.id,
|
|
74
|
-
ssSecret: account.secret,
|
|
75
|
-
encrSecret: encrKeys.secret,
|
|
76
|
-
entropy: account.entropy,
|
|
77
|
-
},
|
|
78
|
-
}));
|
|
79
|
-
})
|
|
80
|
-
.andTee(({ session }) => {
|
|
81
|
-
pairingStatus.write({ step: 'finished', session });
|
|
82
|
-
})
|
|
83
|
-
.orTee(e => {
|
|
84
|
-
if (!(e instanceof AbortError)) {
|
|
85
|
-
pairingStatus.write({ step: 'pairingError', message: e.message });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
const authModule = {
|
|
12
|
+
let abortHandle = null;
|
|
13
|
+
return {
|
|
90
14
|
pairingStatus: readonly(pairingStatus),
|
|
91
15
|
authenticate() {
|
|
92
|
-
if (authResult)
|
|
16
|
+
if (authResult)
|
|
93
17
|
return authResult;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const account = deriveSr25519Account(generateMnemonic(), '//wallet//sso');
|
|
97
|
-
const ssoFlowId = createFlowId();
|
|
18
|
+
const flowId = createFlowId();
|
|
19
|
+
pairingStatus.write({ step: 'initial' });
|
|
98
20
|
emitHostPappDebugMessage({
|
|
99
21
|
layer: 'sso',
|
|
100
22
|
event: 'pairing_started',
|
|
101
|
-
flowId
|
|
23
|
+
flowId,
|
|
102
24
|
timestamp: Date.now(),
|
|
103
|
-
payload: { metadata },
|
|
25
|
+
payload: { metadata: hostMetadata },
|
|
104
26
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
27
|
+
let aborted = false;
|
|
28
|
+
let pairingAbort = null;
|
|
29
|
+
abortHandle = () => {
|
|
30
|
+
aborted = true;
|
|
31
|
+
pairingAbort?.();
|
|
32
|
+
};
|
|
33
|
+
const resolveDeviceIdentity = () => deviceIdentity
|
|
34
|
+
? ResultAsync.fromPromise(Promise.resolve(deviceIdentity()), toError)
|
|
35
|
+
: deviceIdentityStore.loadOrCreate();
|
|
36
|
+
const flow = ResultAsync.combine([
|
|
37
|
+
resolveDeviceIdentity(),
|
|
38
|
+
deviceIdentityStore.readLastProcessedHandshakeStatement(),
|
|
39
|
+
]).andThen(([identity, initialHex]) => {
|
|
40
|
+
if (aborted)
|
|
41
|
+
return okAsync(null);
|
|
42
|
+
const pairing = startPairingV2({
|
|
43
|
+
statementStore,
|
|
44
|
+
deviceIdentity: identity,
|
|
45
|
+
metadata: hostMetadata ?? {},
|
|
46
|
+
initialProcessedDataHex: initialHex,
|
|
47
|
+
onStatementProcessed: hex => {
|
|
48
|
+
void deviceIdentityStore.writeLastProcessedHandshakeStatement(hex);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
pairingAbort = pairing.abort;
|
|
52
|
+
pairingStatus.write({ step: 'pairing', payload: pairing.qrPayload });
|
|
53
|
+
emitHostPappDebugMessage({
|
|
54
|
+
layer: 'sso',
|
|
55
|
+
event: 'deeplink_generated',
|
|
56
|
+
flowId,
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
payload: { deeplink: pairing.qrPayload },
|
|
59
|
+
});
|
|
60
|
+
emitHostPappDebugMessage({
|
|
61
|
+
layer: 'sso',
|
|
62
|
+
event: 'awaiting_response',
|
|
63
|
+
flowId,
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
payload: {},
|
|
66
|
+
});
|
|
67
|
+
return ResultAsync.fromPromise(new Promise((resolve, reject) => {
|
|
68
|
+
let settled = false;
|
|
69
|
+
const settle = (cb) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
settled = true;
|
|
73
|
+
cb();
|
|
74
|
+
};
|
|
75
|
+
const sub = pairing.state$.subscribe({
|
|
76
|
+
next: state => onState(state, s => settle(() => resolve(s)), e => settle(() => reject(e)), () => sub?.unsubscribe()),
|
|
77
|
+
complete: () => {
|
|
78
|
+
if (aborted)
|
|
79
|
+
settle(() => reject(new AbortError('Aborted by user.')));
|
|
80
|
+
},
|
|
81
|
+
error: e => settle(() => reject(toError(e))),
|
|
82
|
+
});
|
|
83
|
+
}), toError).andThen(success => persistAndNotify(identity, success, flowId));
|
|
84
|
+
});
|
|
85
|
+
authResult = flow
|
|
86
|
+
.orElse(e => (e instanceof AbortError ? okAsync(null) : errAsync(e)))
|
|
116
87
|
.andTee(session => {
|
|
117
|
-
if (session) {
|
|
88
|
+
if (session === null) {
|
|
89
|
+
pairingStatus.reset();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
pairingStatus.write({ step: 'finished', session });
|
|
118
93
|
emitHostPappDebugMessage({
|
|
119
94
|
layer: 'sso',
|
|
120
95
|
event: 'session_established',
|
|
121
|
-
flowId
|
|
96
|
+
flowId,
|
|
122
97
|
timestamp: Date.now(),
|
|
123
98
|
payload: { sessionId: session.id },
|
|
124
99
|
});
|
|
125
100
|
}
|
|
126
|
-
|
|
127
|
-
.orElse(e => (e instanceof AbortError ? ok(null) : err(e)))
|
|
128
|
-
.andTee(() => {
|
|
129
|
-
abort = null;
|
|
101
|
+
abortHandle = null;
|
|
130
102
|
})
|
|
131
103
|
.orTee(e => {
|
|
132
|
-
|
|
133
|
-
abort = null;
|
|
104
|
+
pairingStatus.write({ step: 'pairingError', message: e.message });
|
|
134
105
|
emitHostPappDebugMessage({
|
|
135
106
|
layer: 'sso',
|
|
136
107
|
event: 'pairing_failed',
|
|
137
|
-
flowId
|
|
108
|
+
flowId,
|
|
138
109
|
timestamp: Date.now(),
|
|
139
110
|
payload: { reason: e.message },
|
|
140
111
|
});
|
|
112
|
+
authResult = null;
|
|
113
|
+
abortHandle = null;
|
|
141
114
|
});
|
|
142
115
|
return authResult;
|
|
116
|
+
function onState(state, resolve, reject, unsubscribe) {
|
|
117
|
+
switch (state.tag) {
|
|
118
|
+
case 'Idle':
|
|
119
|
+
case 'Submitted':
|
|
120
|
+
return;
|
|
121
|
+
case 'Pending':
|
|
122
|
+
pairingStatus.write({ step: 'pending', stage: state.reason });
|
|
123
|
+
return;
|
|
124
|
+
case 'Success':
|
|
125
|
+
unsubscribe();
|
|
126
|
+
emitHostPappDebugMessage({
|
|
127
|
+
layer: 'sso',
|
|
128
|
+
event: 'response_received',
|
|
129
|
+
flowId,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
payload: { identityAccountId: state.identityAccountId },
|
|
132
|
+
});
|
|
133
|
+
resolve(state);
|
|
134
|
+
return;
|
|
135
|
+
case 'Failed':
|
|
136
|
+
unsubscribe();
|
|
137
|
+
reject(new Error(state.reason));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
143
141
|
},
|
|
144
142
|
abortAuthentication() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
abort = null;
|
|
148
|
-
}
|
|
143
|
+
abortHandle?.();
|
|
144
|
+
abortHandle = null;
|
|
149
145
|
authResult = null;
|
|
150
146
|
pairingStatus.reset();
|
|
151
147
|
},
|
|
152
148
|
};
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const osVersion = hostMetadata?.osVersion;
|
|
160
|
-
return HandshakeData.enc(enumValue('v1', {
|
|
161
|
-
ssPublicKey,
|
|
162
|
-
encrPublicKey,
|
|
163
|
-
metadata,
|
|
164
|
-
hostVersion,
|
|
165
|
-
osType,
|
|
166
|
-
osVersion,
|
|
167
|
-
}));
|
|
168
|
-
}, toError);
|
|
169
|
-
function parseHandshakePayload(payload) {
|
|
170
|
-
const decoded = HandshakeResponsePayload.dec(payload);
|
|
171
|
-
switch (decoded.tag) {
|
|
172
|
-
case 'v1':
|
|
173
|
-
return decoded.value;
|
|
174
|
-
default:
|
|
175
|
-
throw new Error('Unsupported handshake payload version');
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const createEncrKeys = fromThrowable((entropy) => {
|
|
179
|
-
const secret = createEncrSecret(entropy);
|
|
180
|
-
return {
|
|
181
|
-
secret,
|
|
182
|
-
publicKey: getEncrPub(secret),
|
|
183
|
-
};
|
|
184
|
-
}, toError);
|
|
185
|
-
function retrieveSession({ payload, encrSecret, localAccount, }) {
|
|
186
|
-
const { encrypted, tmpKey } = parseHandshakePayload(payload);
|
|
187
|
-
const symmetricKey = createSharedSecret(encrSecret, tmpKey);
|
|
188
|
-
return createEncryption(symmetricKey)
|
|
189
|
-
.decrypt(encrypted)
|
|
190
|
-
.map(decrypted => {
|
|
191
|
-
const { sharedSecretDerivationKey, rootUserAccountId, identityAccountId } = HandshakeResponseSensitiveData.dec(decrypted);
|
|
192
|
-
const sharedSecret = createSharedSecret(encrSecret, sharedSecretDerivationKey);
|
|
193
|
-
const remoteAccount = createRemoteSessionAccount(createAccountId(identityAccountId), sharedSecret);
|
|
194
|
-
return createStoredUserSession(localAccount, remoteAccount, createAccountId(rootUserAccountId));
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
function createDeeplink(payload) {
|
|
198
|
-
return `polkadotapp://pair?handshake=${toHex(payload)}`;
|
|
199
|
-
}
|
|
200
|
-
function waitForStatements(subscribe, signal, callback) {
|
|
201
|
-
return fromPromise(new Promise((resolve, reject) => {
|
|
202
|
-
const unsubscribe = subscribe(statements => {
|
|
203
|
-
const abortError = processSignal(signal).match(() => null, e => e);
|
|
204
|
-
if (abortError) {
|
|
205
|
-
unsubscribe();
|
|
206
|
-
reject(abortError);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
callback(statements, value => {
|
|
211
|
-
unsubscribe();
|
|
212
|
-
resolve(value);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
catch (e) {
|
|
216
|
-
unsubscribe();
|
|
217
|
-
reject(e);
|
|
218
|
-
}
|
|
149
|
+
function persistAndNotify(identity, success, _flowId) {
|
|
150
|
+
const localAccount = createLocalSessionAccount(createAccountId(identity.statementAccountPublicKey));
|
|
151
|
+
const remoteAccount = createRemoteSessionAccount(createAccountId(success.peerStatementAccountId ?? new Uint8Array(32)), success.deviceEncPubKey);
|
|
152
|
+
const session = createStoredUserSession(localAccount, remoteAccount, createAccountId(success.rootAccountId ?? new Uint8Array(32)), {
|
|
153
|
+
identityAccountId: createAccountId(success.identityAccountId),
|
|
154
|
+
identityChatPublicKey: success.identityChatPublicKey,
|
|
219
155
|
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
156
|
+
return userSecretRepository
|
|
157
|
+
.write(session.id, {
|
|
158
|
+
ssSecret: identity.statementAccountSecret,
|
|
159
|
+
encrSecret: identity.encryptionPrivateKey,
|
|
160
|
+
entropy: new Uint8Array(0),
|
|
161
|
+
identityChatPrivateKey: success.identityChatPrivateKey,
|
|
162
|
+
})
|
|
163
|
+
.andThen(() => ssoSessionRepository.add(session))
|
|
164
|
+
.andThen(() => onAuthSuccess
|
|
165
|
+
? ResultAsync.fromPromise(Promise.resolve(onAuthSuccess({ session, identityChatPrivateKey: success.identityChatPrivateKey })), toError).map(() => session)
|
|
166
|
+
: okAsync(session));
|
|
229
167
|
}
|
|
230
168
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCALE codecs for the V2 SSO handshake (multi-device shape).
|
|
3
|
+
*
|
|
4
|
+
* The host emits a `VersionedHandshakeProposal::V2` via QR carrying its
|
|
5
|
+
* `Device { statementAccountId, encryptionPublicKey }` and metadata. The
|
|
6
|
+
* authorising peer (PApp) responds over the Statement Store with a
|
|
7
|
+
* `VersionedHandshakeResponse`; its body is ECDH-encrypted to the host's
|
|
8
|
+
* encryption public key with the peer's ephemeral `tmpKey`. The inner payload
|
|
9
|
+
* after decrypt is `EncryptedHandshakeResponseV2 = Pending | Success | Failed`.
|
|
10
|
+
*
|
|
11
|
+
* `Success` carries:
|
|
12
|
+
* - `identityAccountId` — user identity sr25519 accountId (32 bytes).
|
|
13
|
+
* Adressing for chat / username lookup / session
|
|
14
|
+
* topic derivation.
|
|
15
|
+
* - `rootAccountId` — user root sr25519 accountId (32 bytes). Parent
|
|
16
|
+
* for soft-derivation of product accounts; PApp
|
|
17
|
+
* and host MUST derive identically so a dapp sees
|
|
18
|
+
* the same address on every device.
|
|
19
|
+
* - `identityChatPrivateKey`— user identity chat P-256 private scalar (32 bytes),
|
|
20
|
+
* shared per the multi-device spec so this device
|
|
21
|
+
* can decrypt traffic addressed to the user identity
|
|
22
|
+
* - `deviceEncPubKey` — encryption public key of the PApp device (65 bytes,
|
|
23
|
+
* P-256 uncompressed). Tells the host which key to
|
|
24
|
+
* use when addressing chat envelopes back to the
|
|
25
|
+
* authorising PApp device.
|
|
26
|
+
*
|
|
27
|
+
* Total wire length of `Success` is 32 + 32 + 32 + 65 = 161 bytes. Transit security
|
|
28
|
+
* comes from the outer envelope's ECDH-AES wrap; no per-field signature is
|
|
29
|
+
* carried — multi-device authorisation is asserted by the user-identity-signed
|
|
30
|
+
* roster events (`DeviceAdded`/`DeviceRemoved`) published separately.
|
|
31
|
+
*/
|
|
32
|
+
export declare const MetadataKey: import("scale-ts").Codec<{
|
|
33
|
+
tag: "Custom";
|
|
34
|
+
value: string;
|
|
35
|
+
} | {
|
|
36
|
+
tag: "HostName";
|
|
37
|
+
value: undefined;
|
|
38
|
+
} | {
|
|
39
|
+
tag: "HostVersion";
|
|
40
|
+
value: undefined;
|
|
41
|
+
} | {
|
|
42
|
+
tag: "HostIcon";
|
|
43
|
+
value: undefined;
|
|
44
|
+
} | {
|
|
45
|
+
tag: "PlatformType";
|
|
46
|
+
value: undefined;
|
|
47
|
+
} | {
|
|
48
|
+
tag: "PlatformVersion";
|
|
49
|
+
value: undefined;
|
|
50
|
+
}>;
|
|
51
|
+
export declare const MetadataEntry: import("scale-ts").Codec<[{
|
|
52
|
+
tag: "Custom";
|
|
53
|
+
value: string;
|
|
54
|
+
} | {
|
|
55
|
+
tag: "HostName";
|
|
56
|
+
value: undefined;
|
|
57
|
+
} | {
|
|
58
|
+
tag: "HostVersion";
|
|
59
|
+
value: undefined;
|
|
60
|
+
} | {
|
|
61
|
+
tag: "HostIcon";
|
|
62
|
+
value: undefined;
|
|
63
|
+
} | {
|
|
64
|
+
tag: "PlatformType";
|
|
65
|
+
value: undefined;
|
|
66
|
+
} | {
|
|
67
|
+
tag: "PlatformVersion";
|
|
68
|
+
value: undefined;
|
|
69
|
+
}, string]>;
|
|
70
|
+
export declare const Device: import("scale-ts").Codec<{
|
|
71
|
+
statementAccountId: Uint8Array<ArrayBufferLike>;
|
|
72
|
+
encryptionPublicKey: Uint8Array<ArrayBufferLike>;
|
|
73
|
+
}>;
|
|
74
|
+
export declare const HandshakeProposalV2: import("scale-ts").Codec<{
|
|
75
|
+
device: {
|
|
76
|
+
statementAccountId: Uint8Array<ArrayBufferLike>;
|
|
77
|
+
encryptionPublicKey: Uint8Array<ArrayBufferLike>;
|
|
78
|
+
};
|
|
79
|
+
metadata: [{
|
|
80
|
+
tag: "Custom";
|
|
81
|
+
value: string;
|
|
82
|
+
} | {
|
|
83
|
+
tag: "HostName";
|
|
84
|
+
value: undefined;
|
|
85
|
+
} | {
|
|
86
|
+
tag: "HostVersion";
|
|
87
|
+
value: undefined;
|
|
88
|
+
} | {
|
|
89
|
+
tag: "HostIcon";
|
|
90
|
+
value: undefined;
|
|
91
|
+
} | {
|
|
92
|
+
tag: "PlatformType";
|
|
93
|
+
value: undefined;
|
|
94
|
+
} | {
|
|
95
|
+
tag: "PlatformVersion";
|
|
96
|
+
value: undefined;
|
|
97
|
+
}, string][];
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* V2 lives at SCALE discriminant 1; index 0 is reserved for legacy V1 which
|
|
101
|
+
* neither side emits. The `_v1Reserved` slot pushes V2 to discriminant 1.
|
|
102
|
+
*/
|
|
103
|
+
export declare const VersionedHandshakeProposal: import("scale-ts").Codec<{
|
|
104
|
+
tag: "_v1Reserved";
|
|
105
|
+
value: undefined;
|
|
106
|
+
} | {
|
|
107
|
+
tag: "V2";
|
|
108
|
+
value: {
|
|
109
|
+
device: {
|
|
110
|
+
statementAccountId: Uint8Array<ArrayBufferLike>;
|
|
111
|
+
encryptionPublicKey: Uint8Array<ArrayBufferLike>;
|
|
112
|
+
};
|
|
113
|
+
metadata: [{
|
|
114
|
+
tag: "Custom";
|
|
115
|
+
value: string;
|
|
116
|
+
} | {
|
|
117
|
+
tag: "HostName";
|
|
118
|
+
value: undefined;
|
|
119
|
+
} | {
|
|
120
|
+
tag: "HostVersion";
|
|
121
|
+
value: undefined;
|
|
122
|
+
} | {
|
|
123
|
+
tag: "HostIcon";
|
|
124
|
+
value: undefined;
|
|
125
|
+
} | {
|
|
126
|
+
tag: "PlatformType";
|
|
127
|
+
value: undefined;
|
|
128
|
+
} | {
|
|
129
|
+
tag: "PlatformVersion";
|
|
130
|
+
value: undefined;
|
|
131
|
+
}, string][];
|
|
132
|
+
};
|
|
133
|
+
}>;
|
|
134
|
+
/** 32 + 32 + 32 + 65 = 161 bytes (spec v0.2.1) */
|
|
135
|
+
export declare const HandshakeSuccessV2: import("scale-ts").Codec<{
|
|
136
|
+
identityAccountId: Uint8Array<ArrayBufferLike>;
|
|
137
|
+
rootAccountId: Uint8Array<ArrayBufferLike>;
|
|
138
|
+
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
139
|
+
deviceEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
140
|
+
}>;
|
|
141
|
+
export type HandshakeSuccessV2Value = {
|
|
142
|
+
identityAccountId: Uint8Array;
|
|
143
|
+
/** Nullable for v0.2 peers (Android `feature/location-for-handshake`). */
|
|
144
|
+
rootAccountId: Uint8Array | null;
|
|
145
|
+
identityChatPrivateKey: Uint8Array;
|
|
146
|
+
deviceEncPubKey: Uint8Array;
|
|
147
|
+
};
|
|
148
|
+
/** 32 + 32 + 65 = 129 bytes (spec v0.2 — Android `feature/location-for-handshake`) */
|
|
149
|
+
export declare const HandshakeSuccessV2Legacy: import("scale-ts").Codec<{
|
|
150
|
+
identityAccountId: Uint8Array<ArrayBufferLike>;
|
|
151
|
+
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
152
|
+
deviceEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
153
|
+
}>;
|
|
154
|
+
export type DecodedHandshakeResponseV2 = {
|
|
155
|
+
tag: 'Pending';
|
|
156
|
+
value: {
|
|
157
|
+
tag: 'AllowanceAllocation';
|
|
158
|
+
value: undefined;
|
|
159
|
+
};
|
|
160
|
+
} | {
|
|
161
|
+
tag: 'Success';
|
|
162
|
+
value: HandshakeSuccessV2Value;
|
|
163
|
+
} | {
|
|
164
|
+
tag: 'Failed';
|
|
165
|
+
value: string;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Length-dispatched decoder for the inner `EncryptedHandshakeResponseV2`
|
|
169
|
+
* plaintext. v0.2 (Android) ships 129-byte Success bodies without
|
|
170
|
+
* `rootAccountId`; v0.2.1 ships 161-byte bodies with it. Surfaces
|
|
171
|
+
* `rootAccountId: null` for the legacy case — chat doesn't need it.
|
|
172
|
+
*/
|
|
173
|
+
export declare const decodeEncryptedHandshakeResponseV2: (bytes: Uint8Array) => DecodedHandshakeResponseV2;
|
|
174
|
+
/**
|
|
175
|
+
* Derive the user identity chat P-256 public key from the shared private
|
|
176
|
+
* scalar received in `HandshakeSuccessV2`. Both desktop and mobile must derive
|
|
177
|
+
* identically (uncompressed 65-byte form) so downstream session topics agree.
|
|
178
|
+
*/
|
|
179
|
+
export declare const deriveIdentityChatPublicKey: (privateKey: Uint8Array) => Uint8Array;
|
|
180
|
+
/**
|
|
181
|
+
* Inner Pending sub-statuses. Only `AllowanceAllocation` today.
|
|
182
|
+
*
|
|
183
|
+
* Encoded with its own SCALE discriminant byte — the peer's SCALE library does
|
|
184
|
+
* NOT elide enum-variant indices for sealed interfaces (verified on the wire:
|
|
185
|
+
* `Pending(AllowanceAllocation)` arrives as `0x00 0x00`). Both sides must keep
|
|
186
|
+
* the discriminant when encoding so dispatch agrees.
|
|
187
|
+
*/
|
|
188
|
+
export declare const HandshakeStatusV2: import("scale-ts").Codec<{
|
|
189
|
+
tag: "AllowanceAllocation";
|
|
190
|
+
value: undefined;
|
|
191
|
+
}>;
|
|
192
|
+
export type EncryptedHandshakeResponseV2Value = {
|
|
193
|
+
tag: 'Pending';
|
|
194
|
+
value: {
|
|
195
|
+
tag: 'AllowanceAllocation';
|
|
196
|
+
value: undefined;
|
|
197
|
+
};
|
|
198
|
+
} | {
|
|
199
|
+
tag: 'Success';
|
|
200
|
+
value: HandshakeSuccessV2Value;
|
|
201
|
+
} | {
|
|
202
|
+
tag: 'Failed';
|
|
203
|
+
value: string;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Inner handshake response variant. Wire shape (matches peer SCALE encoding):
|
|
207
|
+
*
|
|
208
|
+
* - Pending → 0x00 || HandshakeStatusV2 (today: 0x00 for AllowanceAllocation), 2 bytes total
|
|
209
|
+
* - Success → 0x01 || HandshakeSuccessV2 (161 bytes), 162 bytes total
|
|
210
|
+
* - Failed → 0x02 || SCALE-encoded UTF-8 reason string
|
|
211
|
+
*
|
|
212
|
+
* Earlier builds shipped a custom length-dispatched codec assuming elision —
|
|
213
|
+
* that assumption was wrong and produced false `Failed("")` reads on every
|
|
214
|
+
* `Pending` response. Native scale-ts `Enum` preserves the discriminant and
|
|
215
|
+
* matches the peer's wire format directly.
|
|
216
|
+
*/
|
|
217
|
+
export declare const EncryptedHandshakeResponseV2: import("scale-ts").Codec<{
|
|
218
|
+
tag: "Pending";
|
|
219
|
+
value: {
|
|
220
|
+
tag: "AllowanceAllocation";
|
|
221
|
+
value: undefined;
|
|
222
|
+
};
|
|
223
|
+
} | {
|
|
224
|
+
tag: "Success";
|
|
225
|
+
value: {
|
|
226
|
+
identityAccountId: Uint8Array<ArrayBufferLike>;
|
|
227
|
+
rootAccountId: Uint8Array<ArrayBufferLike>;
|
|
228
|
+
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
229
|
+
deviceEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
230
|
+
};
|
|
231
|
+
} | {
|
|
232
|
+
tag: "Failed";
|
|
233
|
+
value: string;
|
|
234
|
+
}>;
|
|
235
|
+
export declare const HandshakeResponseV2: import("scale-ts").Codec<{
|
|
236
|
+
encrypted: Uint8Array<ArrayBufferLike>;
|
|
237
|
+
tmpKey: Uint8Array<ArrayBufferLike>;
|
|
238
|
+
}>;
|
|
239
|
+
/** Legacy V1 response shape — decoded for backward compat, never emitted by the V2 path. */
|
|
240
|
+
export declare const HandshakeResponseV1: import("scale-ts").Codec<{
|
|
241
|
+
encrypted: Uint8Array<ArrayBufferLike>;
|
|
242
|
+
tmpKey: Uint8Array<ArrayBufferLike>;
|
|
243
|
+
}>;
|
|
244
|
+
export declare const EncryptedHandshakeResponseV1: import("scale-ts").Codec<{
|
|
245
|
+
encryptionKey: Uint8Array<ArrayBufferLike>;
|
|
246
|
+
accountId: Uint8Array<ArrayBufferLike>;
|
|
247
|
+
}>;
|
|
248
|
+
export declare const VersionedHandshakeResponse: import("scale-ts").Codec<{
|
|
249
|
+
tag: "V2";
|
|
250
|
+
value: {
|
|
251
|
+
encrypted: Uint8Array<ArrayBufferLike>;
|
|
252
|
+
tmpKey: Uint8Array<ArrayBufferLike>;
|
|
253
|
+
};
|
|
254
|
+
} | {
|
|
255
|
+
tag: "V1";
|
|
256
|
+
value: {
|
|
257
|
+
encrypted: Uint8Array<ArrayBufferLike>;
|
|
258
|
+
tmpKey: Uint8Array<ArrayBufferLike>;
|
|
259
|
+
};
|
|
260
|
+
}>;
|