@novasamatech/host-papp 0.8.7-0 → 0.8.7-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/debugBus.js
CHANGED
|
@@ -15,9 +15,14 @@ const bus = createNanoEvents();
|
|
|
15
15
|
let listenerCount = 0;
|
|
16
16
|
/** @internal For host-papp emitters. */
|
|
17
17
|
export function emitHostPappDebugMessage(event) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
try {
|
|
19
|
+
if (listenerCount === 0)
|
|
20
|
+
return;
|
|
21
|
+
bus.emit('message', event);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// do nothing
|
|
25
|
+
}
|
|
21
26
|
}
|
|
22
27
|
/**
|
|
23
28
|
* @internal Lets call sites skip non-trivial payload construction
|
package/dist/index.d.ts
CHANGED
|
@@ -11,3 +11,4 @@ export type { StoredUserSession } from './sso/userSessionRepository.js';
|
|
|
11
11
|
export type { Identity } from './identity/types.js';
|
|
12
12
|
export type { SigningPayloadRequest, SigningPayloadResponse, SigningRawRequest, SigningRequest, } from './sso/sessionManager/scale/signing.js';
|
|
13
13
|
export type { RingVrfAliasRequest, RingVrfAliasResponse } from './sso/sessionManager/scale/ringVrf.js';
|
|
14
|
+
export type { CreateTransactionRequest, CreateTransactionResponse, } from './sso/sessionManager/scale/createTransaction.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSlotAccountProver, deriveSlotAccountPublicKey, ensureSubstrateSlotSr25519Ready, signSlotAccountSecret, } from '@novasamatech/statement-store';
|
|
2
2
|
import { ResultAsync, errAsync, okAsync } from 'neverthrow';
|
|
3
3
|
import { getPolkadotSigner } from 'polkadot-api/signer';
|
|
4
4
|
export class AllowanceError extends Error {
|
|
@@ -56,7 +56,7 @@ export function createAllowanceService({ sessions, repository, }) {
|
|
|
56
56
|
return fetchKey(sessionId, productId, 'bulletin').andThen(secret => ResultAsync.fromPromise(ensureSubstrateSlotSr25519Ready().then(() => getPolkadotSigner(deriveSlotAccountPublicKey(secret), 'Sr25519', input => signSlotAccountSecret(secret, input))), e => new AllowanceError('UnexpectedResponse', e instanceof Error ? e.message : String(e))));
|
|
57
57
|
},
|
|
58
58
|
getStatementStoreProver(sessionId, productId) {
|
|
59
|
-
return fetchKey(sessionId, productId, 'statementStore').
|
|
59
|
+
return fetchKey(sessionId, productId, 'statementStore').andThen(secret => ResultAsync.fromPromise(ensureSubstrateSlotSr25519Ready().then(() => createSlotAccountProver(secret)), e => new AllowanceError('UnexpectedResponse', e instanceof Error ? e.message : String(e))));
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
}
|
|
@@ -19,6 +19,22 @@ function withQueueTimeout(resultAsync, label) {
|
|
|
19
19
|
const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(err(new Error(`${label} timed out — queue freed`))), QUEUE_TASK_TIMEOUT_MS));
|
|
20
20
|
return ResultAsync.fromPromise(Promise.race([resultAsync, timeoutPromise]), toError).andThen(r => r);
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* The transport ACK (`session.request`) and the peer's application `reply`
|
|
24
|
+
* (`session.waitForRequestMessage`) travel on independent channels whose
|
|
25
|
+
* arrival order is non-deterministic. Race them: resolve as soon as the reply
|
|
26
|
+
* arrives, and fail fast if the ACK errors first (e.g. the peer reports a
|
|
27
|
+
* decode failure and will never send a reply). A successful ACK is purely
|
|
28
|
+
* informational here — only the reply completes the call.
|
|
29
|
+
*
|
|
30
|
+
* Callers MUST register `reply` before issuing `request` so the reply
|
|
31
|
+
* subscriber is live before the request goes out.
|
|
32
|
+
*/
|
|
33
|
+
function awaitReplyOrAckFailure(request, reply) {
|
|
34
|
+
// ACK ok → never settle (let the reply win); ACK err → surface immediately.
|
|
35
|
+
const ackFailure = request.andThen(() => ResultAsync.fromSafePromise(new Promise(() => undefined)));
|
|
36
|
+
return ResultAsync.fromSafePromise(Promise.race([reply, ackFailure])).andThen(result => result);
|
|
37
|
+
}
|
|
22
38
|
/**
|
|
23
39
|
* Derive a stable `actionKind` label from a remote-message envelope.
|
|
24
40
|
* Shape: `OuterTag` for flat variants, `OuterTag:InnerTag` for variants
|
|
@@ -100,7 +116,6 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
100
116
|
const messageId = nanoid();
|
|
101
117
|
const data = enumValue('v1', enumValue('SignRequest', enumValue('Payload', payload)));
|
|
102
118
|
emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
|
|
103
|
-
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
104
119
|
const responseFilter = (message) => {
|
|
105
120
|
if (message.data.tag === 'v1' &&
|
|
106
121
|
message.data.value.tag === 'SignResponse' &&
|
|
@@ -108,9 +123,10 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
108
123
|
return message.data.value.value.payload;
|
|
109
124
|
}
|
|
110
125
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
// Register the reply waiter before sending the request (see awaitReplyOrAckFailure).
|
|
127
|
+
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
128
|
+
const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
|
|
129
|
+
const inner = awaitReplyOrAckFailure(request, reply).andThen(message => {
|
|
114
130
|
if (message.success) {
|
|
115
131
|
return ok(message.value);
|
|
116
132
|
}
|
|
@@ -126,7 +142,6 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
126
142
|
const messageId = nanoid();
|
|
127
143
|
const data = enumValue('v1', enumValue('SignRequest', enumValue('Raw', payload)));
|
|
128
144
|
emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
|
|
129
|
-
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
130
145
|
const responseFilter = (message) => {
|
|
131
146
|
if (message.data.tag === 'v1' &&
|
|
132
147
|
message.data.value.tag === 'SignResponse' &&
|
|
@@ -134,9 +149,9 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
134
149
|
return message.data.value.value.payload;
|
|
135
150
|
}
|
|
136
151
|
};
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
153
|
+
const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
|
|
154
|
+
const inner = awaitReplyOrAckFailure(request, reply).andThen(message => {
|
|
140
155
|
if (message.success) {
|
|
141
156
|
return ok(message.value);
|
|
142
157
|
}
|
|
@@ -150,10 +165,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
150
165
|
createTransaction(payload) {
|
|
151
166
|
return enqueue(() => {
|
|
152
167
|
const messageId = nanoid();
|
|
153
|
-
const
|
|
154
|
-
messageId,
|
|
155
|
-
data: enumValue('v1', enumValue('CreateTransactionRequest', payload)),
|
|
156
|
-
});
|
|
168
|
+
const data = enumValue('v1', enumValue('CreateTransactionRequest', payload));
|
|
157
169
|
const responseFilter = (message) => {
|
|
158
170
|
if (message.data.tag === 'v1' &&
|
|
159
171
|
message.data.value.tag === 'CreateTransactionResponse' &&
|
|
@@ -161,9 +173,9 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
161
173
|
return message.data.value.value.signedTransaction;
|
|
162
174
|
}
|
|
163
175
|
};
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
176
|
+
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
177
|
+
const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
|
|
178
|
+
const inner = awaitReplyOrAckFailure(request, reply).andThen(message => {
|
|
167
179
|
if (message.success) {
|
|
168
180
|
return ok(message.value);
|
|
169
181
|
}
|
|
@@ -174,14 +186,6 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
174
186
|
return withQueueTimeout(inner, 'createTransaction');
|
|
175
187
|
});
|
|
176
188
|
},
|
|
177
|
-
sendDisconnectMessage() {
|
|
178
|
-
return enqueue(() => session
|
|
179
|
-
.submitRequestMessage(RemoteMessageCodec, {
|
|
180
|
-
messageId: nanoid(),
|
|
181
|
-
data: enumValue('v1', enumValue('Disconnected', undefined)),
|
|
182
|
-
})
|
|
183
|
-
.map(() => undefined));
|
|
184
|
-
},
|
|
185
189
|
getRingVrfAlias(productAccountId, productId) {
|
|
186
190
|
return enqueue(() => {
|
|
187
191
|
const messageId = nanoid();
|
|
@@ -190,7 +194,6 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
190
194
|
productId,
|
|
191
195
|
}));
|
|
192
196
|
emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
|
|
193
|
-
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
194
197
|
const responseFilter = (message) => {
|
|
195
198
|
if (message.data.tag === 'v1' &&
|
|
196
199
|
message.data.value.tag === 'RingVrfAliasResponse' &&
|
|
@@ -198,18 +201,15 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
198
201
|
return message.data.value.value.payload;
|
|
199
202
|
}
|
|
200
203
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
205
|
+
const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
|
|
206
|
+
return withHostActionTrace(awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(new Error(result.value))), messageId, userSession.id);
|
|
204
207
|
});
|
|
205
208
|
},
|
|
206
|
-
requestResourceAllocation(
|
|
209
|
+
requestResourceAllocation(payload) {
|
|
207
210
|
return enqueue(() => {
|
|
208
211
|
const messageId = nanoid();
|
|
209
|
-
const
|
|
210
|
-
messageId,
|
|
211
|
-
data: enumValue('v1', enumValue('ResourceAllocationRequest', request)),
|
|
212
|
-
});
|
|
212
|
+
const data = enumValue('v1', enumValue('ResourceAllocationRequest', payload));
|
|
213
213
|
const responseFilter = (message) => {
|
|
214
214
|
if (message.data.tag === 'v1' &&
|
|
215
215
|
message.data.value.tag === 'ResourceAllocationResponse' &&
|
|
@@ -217,71 +217,80 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
217
217
|
return message.data.value.value.payload;
|
|
218
218
|
}
|
|
219
219
|
};
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
221
|
+
const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
|
|
222
|
+
const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(new Error(result.value)));
|
|
223
223
|
return withQueueTimeout(inner, 'requestResourceAllocation');
|
|
224
224
|
});
|
|
225
225
|
},
|
|
226
|
+
sendDisconnectMessage() {
|
|
227
|
+
return enqueue(() => session
|
|
228
|
+
.submitRequestMessage(RemoteMessageCodec, {
|
|
229
|
+
messageId: nanoid(),
|
|
230
|
+
data: enumValue('v1', enumValue('Disconnected', undefined)),
|
|
231
|
+
})
|
|
232
|
+
.map(() => undefined));
|
|
233
|
+
},
|
|
226
234
|
subscribe(callback) {
|
|
227
|
-
|
|
228
|
-
|
|
235
|
+
// App-level side effects for one decoded incoming request: dedup, invoke
|
|
236
|
+
// the consumer callback, emit debug events, and persist the processed id.
|
|
237
|
+
// Run independently of the ACK (see respondToRequests handler below) so a
|
|
238
|
+
// forgotten, failed, or duplicate handler never blocks the response we owe
|
|
239
|
+
// the peer.
|
|
240
|
+
const runSideEffects = (value) => {
|
|
241
|
+
const messageId = value.messageId;
|
|
242
|
+
void processedMessages
|
|
229
243
|
.read()
|
|
230
244
|
.andThen(processed => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
245
|
+
if (processed.includes(messageId))
|
|
246
|
+
return okAsync(undefined);
|
|
247
|
+
const actionKind = actionKindFromMessageData(value.data);
|
|
248
|
+
emitHostPappDebugMessage({
|
|
249
|
+
layer: 'session',
|
|
250
|
+
event: 'peer_action_received',
|
|
251
|
+
flowId: messageId,
|
|
252
|
+
timestamp: Date.now(),
|
|
253
|
+
payload: { sessionId: userSession.id, messageId, actionKind },
|
|
254
|
+
});
|
|
255
|
+
return callback(value)
|
|
256
|
+
.andTee(processed => {
|
|
257
|
+
if (processed) {
|
|
240
258
|
emitHostPappDebugMessage({
|
|
241
259
|
layer: 'session',
|
|
242
|
-
event: '
|
|
260
|
+
event: 'peer_action_processed',
|
|
243
261
|
flowId: messageId,
|
|
244
262
|
timestamp: Date.now(),
|
|
245
|
-
payload: { sessionId: userSession.id, messageId
|
|
263
|
+
payload: { sessionId: userSession.id, messageId },
|
|
246
264
|
});
|
|
247
|
-
return callback(payload.value)
|
|
248
|
-
.andTee(processed => {
|
|
249
|
-
if (processed) {
|
|
250
|
-
emitHostPappDebugMessage({
|
|
251
|
-
layer: 'session',
|
|
252
|
-
event: 'peer_action_processed',
|
|
253
|
-
flowId: messageId,
|
|
254
|
-
timestamp: Date.now(),
|
|
255
|
-
payload: { sessionId: userSession.id, messageId },
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
})
|
|
259
|
-
.orTee(error => {
|
|
260
|
-
console.error('Error while processing sso message:', error);
|
|
261
|
-
emitHostPappDebugMessage({
|
|
262
|
-
layer: 'session',
|
|
263
|
-
event: 'peer_action_failed',
|
|
264
|
-
flowId: messageId,
|
|
265
|
-
timestamp: Date.now(),
|
|
266
|
-
payload: { sessionId: userSession.id, messageId, reason: error.message },
|
|
267
|
-
});
|
|
268
|
-
})
|
|
269
|
-
.orElse(() => okAsync(false))
|
|
270
|
-
.map(processed => (processed ? { processed, message: payload.value } : { processed }));
|
|
271
|
-
}
|
|
272
|
-
return okAsync({ processed: false });
|
|
273
|
-
});
|
|
274
|
-
return ResultAsync.combine(results).andThen(results => {
|
|
275
|
-
const newMessages = results.filter(x => x.processed).map(x => x.message.messageId);
|
|
276
|
-
if (newMessages.length > 0) {
|
|
277
|
-
return processedMessages.mutate(x => x.concat(newMessages));
|
|
278
265
|
}
|
|
279
|
-
|
|
280
|
-
|
|
266
|
+
})
|
|
267
|
+
.orTee(error => {
|
|
268
|
+
console.error('Error while processing sso message:', error);
|
|
269
|
+
emitHostPappDebugMessage({
|
|
270
|
+
layer: 'session',
|
|
271
|
+
event: 'peer_action_failed',
|
|
272
|
+
flowId: messageId,
|
|
273
|
+
timestamp: Date.now(),
|
|
274
|
+
payload: { sessionId: userSession.id, messageId, reason: error.message },
|
|
275
|
+
});
|
|
276
|
+
})
|
|
277
|
+
.orElse(() => okAsync(false))
|
|
278
|
+
.andThen(processed => processed ? processedMessages.mutate(x => x.concat(messageId)) : okAsync(undefined));
|
|
281
279
|
})
|
|
282
280
|
.orTee(error => {
|
|
283
281
|
console.error('Error while updating processed sso messages:', error);
|
|
284
282
|
});
|
|
283
|
+
};
|
|
284
|
+
// The session auto-submits the ResponseStatus this handler returns, so an
|
|
285
|
+
// incoming request can never go unanswered. The ACK is transport-level:
|
|
286
|
+
// the statement already decrypted to be delivered, so the only failure
|
|
287
|
+
// observable here is a decode failure. Always answer — even a duplicate —
|
|
288
|
+
// because a peer retransmit means it never saw our previous ACK.
|
|
289
|
+
return session.respondToRequests(RemoteMessageCodec, message => {
|
|
290
|
+
if (message.payload.status !== 'parsed')
|
|
291
|
+
return 'decodingFailed';
|
|
292
|
+
runSideEffects(message.payload.value);
|
|
293
|
+
return 'success';
|
|
285
294
|
});
|
|
286
295
|
},
|
|
287
296
|
abortPendingRequests() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novasamatech/host-papp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.8.7-
|
|
4
|
+
"version": "0.8.7-1",
|
|
5
5
|
"description": "Polkadot app integration",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"@noble/ciphers": "2.2.0",
|
|
35
35
|
"@noble/curves": "2.2.0",
|
|
36
36
|
"@noble/hashes": "2.2.0",
|
|
37
|
-
"@novasamatech/host-api": "0.8.7-
|
|
38
|
-
"@novasamatech/scale": "0.8.7-
|
|
39
|
-
"@novasamatech/statement-store": "0.8.7-
|
|
40
|
-
"@novasamatech/storage-adapter": "0.8.7-
|
|
37
|
+
"@novasamatech/host-api": "0.8.7-1",
|
|
38
|
+
"@novasamatech/scale": "0.8.7-1",
|
|
39
|
+
"@novasamatech/statement-store": "0.8.7-1",
|
|
40
|
+
"@novasamatech/storage-adapter": "0.8.7-1",
|
|
41
41
|
"@polkadot-api/utils": "^0.4.0",
|
|
42
42
|
"@polkadot-labs/hdkd-helpers": "^0.0.30",
|
|
43
43
|
"nanoevents": "9.1.0",
|