@novasamatech/host-papp 0.8.0-1 → 0.8.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/debug-public.d.ts +1 -2
- package/dist/debug-public.js +1 -2
- package/dist/helpers/createAsyncTaskPool.d.ts +1 -0
- package/dist/helpers/createAsyncTaskPool.js +33 -6
- package/dist/helpers/createAsyncTaskPool.spec.js +44 -0
- package/dist/identity/impl.d.ts +2 -1
- package/dist/identity/impl.js +72 -2
- package/dist/identity/rpcAdapter.js +43 -34
- package/dist/identity/types.d.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/papp.d.ts +2 -0
- package/dist/papp.js +11 -1
- package/dist/sso/allowance/impl.d.ts +24 -0
- package/dist/sso/allowance/impl.js +70 -0
- package/dist/sso/allowance/index.d.ts +4 -0
- package/dist/sso/allowance/index.js +2 -0
- package/dist/sso/allowance/repository.d.ts +9 -0
- package/dist/sso/allowance/repository.js +60 -0
- package/dist/sso/auth/scale/handshakeV2.d.ts +3 -3
- package/dist/sso/sessionManager/impl.d.ts +3 -1
- package/dist/sso/sessionManager/impl.js +3 -2
- package/dist/sso/sessionManager/scale/remoteMessage.d.ts +8 -8
- package/dist/sso/sessionManager/scale/resourceAllocation.d.ts +21 -21
- package/dist/sso/sessionManager/userSession.d.ts +1 -0
- package/dist/sso/sessionManager/userSession.js +27 -6
- package/package.json +7 -8
package/dist/debug-public.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lightweight public entry for the debug bus. Lives outside the main
|
|
3
3
|
* `index.ts` so consumers can import the debug surface without
|
|
4
|
-
* loading the
|
|
5
|
-
* verifiablejs WASM that can't initialise in all environments.
|
|
4
|
+
* loading the session-manager module.
|
|
6
5
|
*/
|
|
7
6
|
export { emitHostPappDebugMessage, hasHostPappDebugListeners, onHostPappDebugMessage } from './debugBus.js';
|
|
8
7
|
export type { AttestationDebugEvent, HostPappDebugEvent, SessionDebugEvent, SsoDebugEvent } from './debugTypes.js';
|
package/dist/debug-public.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lightweight public entry for the debug bus. Lives outside the main
|
|
3
3
|
* `index.ts` so consumers can import the debug surface without
|
|
4
|
-
* loading the
|
|
5
|
-
* verifiablejs WASM that can't initialise in all environments.
|
|
4
|
+
* loading the session-manager module.
|
|
6
5
|
*/
|
|
7
6
|
export { emitHostPappDebugMessage, hasHostPappDebugListeners, onHostPappDebugMessage } from './debugBus.js';
|
|
@@ -20,6 +20,7 @@ declare class AsyncTaskPool {
|
|
|
20
20
|
constructor(config: Params);
|
|
21
21
|
call<T>(fn: () => ResultAsync<T, Error>, params?: TaskParams): ResultAsync<T, Error>;
|
|
22
22
|
settle(pool: string): Promise<void>;
|
|
23
|
+
private abortTask;
|
|
23
24
|
private processPool;
|
|
24
25
|
private tryToSettlePool;
|
|
25
26
|
private retryDelay;
|
|
@@ -3,6 +3,9 @@ import { fromPromise } from 'neverthrow';
|
|
|
3
3
|
import { promiseWithResolvers } from './promiseWithResolvers.js';
|
|
4
4
|
import { nullable, toError } from './utils.js';
|
|
5
5
|
export const DEFAULT_POOL = 'default';
|
|
6
|
+
function abortReason(signal) {
|
|
7
|
+
return signal.reason instanceof Error ? signal.reason : new Error('Task aborted');
|
|
8
|
+
}
|
|
6
9
|
/**
|
|
7
10
|
* Task manager with queues, retries and named pools.
|
|
8
11
|
*/
|
|
@@ -16,14 +19,21 @@ class AsyncTaskPool {
|
|
|
16
19
|
}
|
|
17
20
|
call(fn, params) {
|
|
18
21
|
const { resolve, reject, promise } = promiseWithResolvers();
|
|
22
|
+
const signal = params?.signal;
|
|
23
|
+
// An already-aborted signal never enqueues the task — reject up-front.
|
|
24
|
+
if (signal?.aborted) {
|
|
25
|
+
reject(abortReason(signal));
|
|
26
|
+
return fromPromise(promise, toError);
|
|
27
|
+
}
|
|
19
28
|
const task = {
|
|
20
29
|
fn,
|
|
21
30
|
pool: params?.pool ?? DEFAULT_POOL,
|
|
22
31
|
retry: 0,
|
|
23
32
|
resolve,
|
|
24
33
|
reject,
|
|
25
|
-
signal
|
|
34
|
+
signal,
|
|
26
35
|
};
|
|
36
|
+
signal?.addEventListener('abort', () => this.abortTask(task), { once: true });
|
|
27
37
|
this.queue.push(task);
|
|
28
38
|
this.processPool(task.pool);
|
|
29
39
|
return fromPromise(promise, toError);
|
|
@@ -42,6 +52,23 @@ class AsyncTaskPool {
|
|
|
42
52
|
const unsubscribe = this.events.on('settled', handler);
|
|
43
53
|
});
|
|
44
54
|
}
|
|
55
|
+
// Drop a task because its signal aborted, whether it is still queued, waiting on a
|
|
56
|
+
// retry timer, or already running. Rejecting a promise that later settles on its own
|
|
57
|
+
// is a no-op, so the in-flight fn finishing afterwards can't override this rejection.
|
|
58
|
+
abortTask(task) {
|
|
59
|
+
if (task.retryTimeout !== undefined) {
|
|
60
|
+
clearTimeout(task.retryTimeout);
|
|
61
|
+
}
|
|
62
|
+
const queueIndex = this.queue.indexOf(task);
|
|
63
|
+
if (queueIndex >= 0) {
|
|
64
|
+
this.queue.splice(queueIndex, 1);
|
|
65
|
+
}
|
|
66
|
+
this.activeTasks = this.activeTasks.filter(x => x !== task);
|
|
67
|
+
task.reject(task.signal ? abortReason(task.signal) : new Error('Task aborted'));
|
|
68
|
+
// Free the slot the task held (or would have held) so the pool keeps draining.
|
|
69
|
+
this.processPool(task.pool);
|
|
70
|
+
this.tryToSettlePool(task.pool);
|
|
71
|
+
}
|
|
45
72
|
processPool(pool) {
|
|
46
73
|
let task = null;
|
|
47
74
|
const activeTasks = this.activeTasks.filter(x => x.pool === pool);
|
|
@@ -63,17 +90,17 @@ class AsyncTaskPool {
|
|
|
63
90
|
}
|
|
64
91
|
this.activeTasks.push(task);
|
|
65
92
|
const handleError = (task, error) => {
|
|
93
|
+
// The task was already rejected by abortTask; don't reject again or schedule a retry.
|
|
94
|
+
if (task.signal?.aborted) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
66
97
|
if (task.retry >= this.config.retryCount) {
|
|
67
98
|
task.reject(error);
|
|
68
99
|
}
|
|
69
100
|
else {
|
|
70
|
-
if (task.signal?.aborted) {
|
|
71
|
-
task.reject(error);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
101
|
const retryDelay = this.retryDelay(task);
|
|
75
102
|
task.retry++;
|
|
76
|
-
setTimeout(() => {
|
|
103
|
+
task.retryTimeout = setTimeout(() => {
|
|
77
104
|
this.queue.push(task);
|
|
78
105
|
this.processPool(pool);
|
|
79
106
|
}, retryDelay);
|
|
@@ -115,6 +115,50 @@ describe('asyncTaskPool', () => {
|
|
|
115
115
|
await pool.settle('test');
|
|
116
116
|
expect(result).toEqual([1, 2, 3, 4]);
|
|
117
117
|
});
|
|
118
|
+
describe('abort signal', () => {
|
|
119
|
+
it('rejects a queued task when the signal aborts, without ever running it', async () => {
|
|
120
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const queuedSpy = vi.fn(() => okAsync('queued'));
|
|
123
|
+
// Occupy the single slot with a slow task so the next call has to queue.
|
|
124
|
+
const active = pool.call(() => fromPromise(delay(50).then(() => 'active'), toError));
|
|
125
|
+
const queued = pool.call(queuedSpy, { signal: controller.signal });
|
|
126
|
+
controller.abort();
|
|
127
|
+
const queuedResult = await queued;
|
|
128
|
+
expect(queuedResult.isErr()).toBe(true);
|
|
129
|
+
expect(queuedSpy).not.toHaveBeenCalled();
|
|
130
|
+
expect((await active).isOk()).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
it('rejects the in-flight active task when the signal aborts', async () => {
|
|
133
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const active = pool.call(() => fromPromise(delay(10_000).then(() => 'done'), toError), {
|
|
136
|
+
signal: controller.signal,
|
|
137
|
+
});
|
|
138
|
+
controller.abort();
|
|
139
|
+
expect((await active).isErr()).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
it('frees the slot for later tasks after an abort', async () => {
|
|
142
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const aborted = pool.call(() => fromPromise(delay(10_000).then(() => 'never'), toError), {
|
|
145
|
+
signal: controller.signal,
|
|
146
|
+
});
|
|
147
|
+
controller.abort();
|
|
148
|
+
await aborted;
|
|
149
|
+
const next = await pool.call(() => okAsync('next'));
|
|
150
|
+
expect(next).toEqual(ok('next'));
|
|
151
|
+
});
|
|
152
|
+
it('rejects immediately when called with an already-aborted signal', async () => {
|
|
153
|
+
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
controller.abort();
|
|
156
|
+
const spy = vi.fn(() => okAsync('x'));
|
|
157
|
+
const result = await pool.call(spy, { signal: controller.signal });
|
|
158
|
+
expect(result.isErr()).toBe(true);
|
|
159
|
+
expect(spy).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
118
162
|
it('should settle tasks that was created by chain reaction', async () => {
|
|
119
163
|
const pool = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
120
164
|
const tasks = [
|
package/dist/identity/impl.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { StorageAdapter } from '@novasamatech/storage-adapter';
|
|
2
2
|
import type { IdentityAdapter, IdentityRepository } from './types.js';
|
|
3
|
-
export declare function createIdentityRepository({ adapter, storage, }: {
|
|
3
|
+
export declare function createIdentityRepository({ adapter, storage, initialEmissionTimeoutMs, }: {
|
|
4
4
|
adapter: IdentityAdapter;
|
|
5
5
|
storage: StorageAdapter;
|
|
6
|
+
initialEmissionTimeoutMs?: number;
|
|
6
7
|
}): IdentityRepository;
|
package/dist/identity/impl.js
CHANGED
|
@@ -1,7 +1,67 @@
|
|
|
1
1
|
import { Result, ResultAsync, err, ok, okAsync } from 'neverthrow';
|
|
2
|
+
import { defer, distinctUntilChanged, filter, finalize, map, merge, shareReplay, takeUntil, tap, timer } from 'rxjs';
|
|
2
3
|
import { toError } from '../helpers/utils.js';
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
const WATCH_IDENTITY_INITIAL_TIMEOUT_MS = 15_000;
|
|
5
|
+
function getCacheKey(accountId) {
|
|
6
|
+
return `identity_${accountId}`;
|
|
7
|
+
}
|
|
8
|
+
function parseIdentity(raw) {
|
|
9
|
+
if (!raw)
|
|
10
|
+
return null;
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Identity values are produced by `decodeRawIdentity` only, so key order is
|
|
19
|
+
// deterministic and JSON.stringify is a safe structural equality probe.
|
|
20
|
+
function identitiesEqual(a, b) {
|
|
21
|
+
if (a === b)
|
|
22
|
+
return true;
|
|
23
|
+
if (a === null || b === null)
|
|
24
|
+
return false;
|
|
25
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
26
|
+
}
|
|
27
|
+
function readCachedIdentity(storage, accountId) {
|
|
28
|
+
return defer(() => storage.read(getCacheKey(accountId)).match(parseIdentity, () => null));
|
|
29
|
+
}
|
|
30
|
+
export function createIdentityRepository({ adapter, storage, initialEmissionTimeoutMs = WATCH_IDENTITY_INITIAL_TIMEOUT_MS, }) {
|
|
31
|
+
const cachedRequester = createCachedIdentityRequester(storage, getCacheKey);
|
|
32
|
+
// Per-account de-dup: concurrent watchIdentity(acc) calls share one chain
|
|
33
|
+
// subscription via the shared stream built below. The entry clears itself —
|
|
34
|
+
// see the `finalize` in `buildWatch` — so the map can't accumulate dead
|
|
35
|
+
// streams across distinct accounts.
|
|
36
|
+
const watchCache = new Map();
|
|
37
|
+
function buildWatch(accountId) {
|
|
38
|
+
const live$ = adapter.watchIdentity(accountId).pipe(distinctUntilChanged(identitiesEqual), tap(identity => {
|
|
39
|
+
if (identity === null)
|
|
40
|
+
return;
|
|
41
|
+
// Best-effort write-through; failures must not surface on the read.
|
|
42
|
+
void storage.write(getCacheKey(accountId), JSON.stringify(identity));
|
|
43
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
44
|
+
// Seed from cache so warm consumers paint a value before the first chain
|
|
45
|
+
// block. Only a non-null seed is emitted: a cold cache must fall through to
|
|
46
|
+
// the live read / fallback rather than surface a premature `null` (that
|
|
47
|
+
// also keeps the fallback timer below reachable). `shareReplay` keeps the
|
|
48
|
+
// single storage read from being duplicated by the fallback's `takeUntil`.
|
|
49
|
+
const seed$ = readCachedIdentity(storage, accountId).pipe(filter((identity) => identity !== null), takeUntil(live$), shareReplay({ bufferSize: 1, refCount: true }));
|
|
50
|
+
// Cold-cache + silent-chain safety net: emit `null` so the UI doesn't hang.
|
|
51
|
+
const fallback$ = timer(initialEmissionTimeoutMs).pipe(takeUntil(merge(seed$, live$)), map(() => null));
|
|
52
|
+
const stream = merge(seed$, live$, fallback$).pipe(distinctUntilChanged(identitiesEqual),
|
|
53
|
+
// refCount tears down the chain subscription when the last subscriber
|
|
54
|
+
// leaves; `finalize` then drops the map entry so a later watch rebuilds
|
|
55
|
+
// a fresh stream instead of reusing a dead one. The shared `shareReplay`
|
|
56
|
+
// guarantees this fires once, on the final unsubscribe — not per caller.
|
|
57
|
+
// Guard on identity: a late re-subscribe to an already torn-down stream
|
|
58
|
+
// must not evict a newer entry that another caller built for this account.
|
|
59
|
+
finalize(() => {
|
|
60
|
+
if (watchCache.get(accountId) === stream)
|
|
61
|
+
watchCache.delete(accountId);
|
|
62
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
63
|
+
return stream;
|
|
64
|
+
}
|
|
5
65
|
return {
|
|
6
66
|
getIdentity(accountId) {
|
|
7
67
|
return cachedRequester([accountId], adapter.readIdentities).map(map => map[accountId] ?? null);
|
|
@@ -9,6 +69,16 @@ export function createIdentityRepository({ adapter, storage, }) {
|
|
|
9
69
|
getIdentities(accounts) {
|
|
10
70
|
return cachedRequester(accounts, adapter.readIdentities);
|
|
11
71
|
},
|
|
72
|
+
// Adapter errors propagate as-is; callers must attach an error handler
|
|
73
|
+
// and re-subscribe to recover (no automatic retry).
|
|
74
|
+
watchIdentity(accountId) {
|
|
75
|
+
const existing = watchCache.get(accountId);
|
|
76
|
+
if (existing)
|
|
77
|
+
return existing;
|
|
78
|
+
const stream = buildWatch(accountId);
|
|
79
|
+
watchCache.set(accountId, stream);
|
|
80
|
+
return stream;
|
|
81
|
+
},
|
|
12
82
|
};
|
|
13
83
|
}
|
|
14
84
|
function createCachedIdentityRequester(storage, getKey) {
|
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
import { errAsync, fromPromise, ok } from 'neverthrow';
|
|
2
2
|
import { AccountId } from 'polkadot-api';
|
|
3
|
+
import { defer, map, throwError } from 'rxjs';
|
|
3
4
|
import { toError } from '../helpers/utils.js';
|
|
4
5
|
import { zipWith } from '../helpers/zipWith.js';
|
|
6
|
+
function decodeRawIdentity(accountId, raw, textDecoder) {
|
|
7
|
+
if (!raw)
|
|
8
|
+
return null;
|
|
9
|
+
const credibility = raw.credibility.type === 'Lite'
|
|
10
|
+
? { type: 'Lite' }
|
|
11
|
+
: {
|
|
12
|
+
type: 'Person',
|
|
13
|
+
alias: raw.credibility.value.alias,
|
|
14
|
+
lastUpdate: raw.credibility.value.last_update.toString(),
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
accountId,
|
|
18
|
+
fullUsername: raw.full_username ? textDecoder.decode(raw.full_username) : null,
|
|
19
|
+
liteUsername: textDecoder.decode(raw.lite_username),
|
|
20
|
+
credibility,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
5
23
|
export function createIdentityRpcAdapter(lazyClient) {
|
|
6
24
|
const accCodec = AccountId();
|
|
25
|
+
const textDecoder = new TextDecoder();
|
|
26
|
+
function getConsumersStorage() {
|
|
27
|
+
const client = lazyClient.getClient();
|
|
28
|
+
const unsafeApi = client.getUnsafeApi();
|
|
29
|
+
return unsafeApi.query.Resources.Consumers;
|
|
30
|
+
}
|
|
7
31
|
return {
|
|
8
32
|
readIdentities(accounts) {
|
|
9
|
-
const
|
|
10
|
-
const client = lazyClient.getClient();
|
|
11
|
-
const unsafeApi = client.getUnsafeApi();
|
|
12
|
-
const method = unsafeApi.query.Resources.Consumers;
|
|
33
|
+
const method = getConsumersStorage();
|
|
13
34
|
if (!method) {
|
|
14
35
|
return errAsync(new Error('Method Resources.Consumers not found'));
|
|
15
36
|
}
|
|
@@ -18,36 +39,24 @@ export function createIdentityRpcAdapter(lazyClient) {
|
|
|
18
39
|
if (!results) {
|
|
19
40
|
return ok({});
|
|
20
41
|
}
|
|
21
|
-
return ok(Object.fromEntries(zipWith([accounts, results], x => x).map(([accountId, typedRaw]) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
raw.credibility.value.lastUpdate).toString(),
|
|
40
|
-
};
|
|
41
|
-
return [
|
|
42
|
-
accountId,
|
|
43
|
-
{
|
|
44
|
-
accountId: accountId,
|
|
45
|
-
fullUsername: fullUsername ? textDecoder.decode(fullUsername) : null,
|
|
46
|
-
liteUsername: liteUsername ? textDecoder.decode(liteUsername) : '',
|
|
47
|
-
credibility,
|
|
48
|
-
},
|
|
49
|
-
];
|
|
50
|
-
})));
|
|
42
|
+
return ok(Object.fromEntries(zipWith([accounts, results], x => x).map(([accountId, typedRaw]) => [
|
|
43
|
+
accountId,
|
|
44
|
+
decodeRawIdentity(accountId, typedRaw, textDecoder),
|
|
45
|
+
])));
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
watchIdentity(accountId) {
|
|
49
|
+
// `defer` so client resolution and key decoding run on subscribe and any
|
|
50
|
+
// failure surfaces as a stream error, not a synchronous throw at the call
|
|
51
|
+
// site (the consumer only attaches its error handler via `.subscribe`).
|
|
52
|
+
return defer(() => {
|
|
53
|
+
const method = getConsumersStorage();
|
|
54
|
+
if (!method) {
|
|
55
|
+
return throwError(() => new Error('Method Resources.Consumers not found'));
|
|
56
|
+
}
|
|
57
|
+
return method
|
|
58
|
+
.watchValue(accCodec.dec(accountId))
|
|
59
|
+
.pipe(map(emission => decodeRawIdentity(accountId, emission.value, textDecoder)));
|
|
51
60
|
});
|
|
52
61
|
},
|
|
53
62
|
};
|
package/dist/identity/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResultAsync } from 'neverthrow';
|
|
2
|
+
import type { Observable } from 'rxjs';
|
|
2
3
|
export type Credibility = {
|
|
3
4
|
type: 'Lite';
|
|
4
5
|
} | {
|
|
@@ -14,8 +15,10 @@ export type Identity = {
|
|
|
14
15
|
};
|
|
15
16
|
export type IdentityAdapter = {
|
|
16
17
|
readIdentities(accounts: string[]): ResultAsync<Record<string, Identity | null>, Error>;
|
|
18
|
+
watchIdentity(accountId: string): Observable<Identity | null>;
|
|
17
19
|
};
|
|
18
20
|
export type IdentityRepository = {
|
|
19
21
|
getIdentity(accountId: string): ResultAsync<Identity | null, Error>;
|
|
20
22
|
getIdentities(accounts: string[]): ResultAsync<Record<string, Identity | null>, Error>;
|
|
23
|
+
watchIdentity(accountId: string): Observable<Identity | null>;
|
|
21
24
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { createPappAdapter } from './papp.js';
|
|
|
4
4
|
export type { AuthComponent, HostMetadata, OnAuthSuccess } from './sso/auth/impl.js';
|
|
5
5
|
export type { PairingStatus } from './sso/auth/types.js';
|
|
6
6
|
export type { DeviceIdentityForPairing } from './sso/auth/v2/service.js';
|
|
7
|
+
export type { AllowanceErrorReason, AllowanceService } from './sso/allowance/index.js';
|
|
8
|
+
export { AllowanceError } from './sso/allowance/index.js';
|
|
7
9
|
export type { UserSession } from './sso/sessionManager/userSession.js';
|
|
8
10
|
export type { StoredUserSession } from './sso/userSessionRepository.js';
|
|
9
11
|
export type { Identity } from './identity/types.js';
|
package/dist/index.js
CHANGED
package/dist/papp.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LazyClient, StatementStoreAdapter } from '@novasamatech/statement-store';
|
|
2
2
|
import type { StorageAdapter } from '@novasamatech/storage-adapter';
|
|
3
3
|
import type { IdentityAdapter, IdentityRepository } from './identity/types.js';
|
|
4
|
+
import type { AllowanceService } from './sso/allowance/index.js';
|
|
4
5
|
import type { AuthComponent, HostMetadata, OnAuthSuccess } from './sso/auth/impl.js';
|
|
5
6
|
import type { DeviceIdentityForPairing } from './sso/auth/v2/service.js';
|
|
6
7
|
import type { SsoSessionManager } from './sso/sessionManager/impl.js';
|
|
@@ -10,6 +11,7 @@ export type PappAdapter = {
|
|
|
10
11
|
sessions: SsoSessionManager;
|
|
11
12
|
secrets: UserSecretRepository;
|
|
12
13
|
identity: IdentityRepository;
|
|
14
|
+
allowance: AllowanceService;
|
|
13
15
|
};
|
|
14
16
|
type Adapters = {
|
|
15
17
|
statementStore: StatementStoreAdapter;
|
package/dist/papp.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getWsProvider } from 'polkadot-api/ws';
|
|
|
4
4
|
import { SS_STABLE_STAGE_ENDPOINTS } from './constants.js';
|
|
5
5
|
import { createIdentityRepository } from './identity/impl.js';
|
|
6
6
|
import { createIdentityRpcAdapter } from './identity/rpcAdapter.js';
|
|
7
|
+
import { createAllowanceRepository, createAllowanceService } from './sso/allowance/index.js';
|
|
7
8
|
import { createAuth } from './sso/auth/impl.js';
|
|
8
9
|
import { createDeviceIdentityStore } from './sso/deviceIdentityStore.js';
|
|
9
10
|
import { createSsoSessionManager } from './sso/sessionManager/impl.js';
|
|
@@ -17,7 +18,15 @@ export function createPappAdapter({ appId, hostMetadata, deviceIdentity, onAuthS
|
|
|
17
18
|
const storage = adapters?.storage ?? createLocalStorageAdapter(appId);
|
|
18
19
|
const ssoSessionRepository = createUserSessionRepository(storage);
|
|
19
20
|
const userSecretRepository = createUserSecretRepository(appId, storage);
|
|
21
|
+
const allowanceRepository = createAllowanceRepository(appId, storage);
|
|
20
22
|
const deviceIdentityStore = createDeviceIdentityStore(appId, storage);
|
|
23
|
+
const sessions = createSsoSessionManager({
|
|
24
|
+
storage,
|
|
25
|
+
statementStore,
|
|
26
|
+
ssoSessionRepository,
|
|
27
|
+
userSecretRepository,
|
|
28
|
+
allowanceRepository,
|
|
29
|
+
});
|
|
21
30
|
return {
|
|
22
31
|
sso: createAuth({
|
|
23
32
|
hostMetadata,
|
|
@@ -28,8 +37,9 @@ export function createPappAdapter({ appId, hostMetadata, deviceIdentity, onAuthS
|
|
|
28
37
|
userSecretRepository,
|
|
29
38
|
onAuthSuccess,
|
|
30
39
|
}),
|
|
31
|
-
sessions
|
|
40
|
+
sessions,
|
|
32
41
|
secrets: userSecretRepository,
|
|
33
42
|
identity: createIdentityRepository({ adapter: identities, storage }),
|
|
43
|
+
allowance: createAllowanceService({ sessions: sessions.sessions, repository: allowanceRepository }),
|
|
34
44
|
};
|
|
35
45
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StatementProver } from '@novasamatech/statement-store';
|
|
2
|
+
import { ResultAsync } from 'neverthrow';
|
|
3
|
+
import type { PolkadotSigner } from 'polkadot-api/signer';
|
|
4
|
+
import type { Callback } from '../../types.js';
|
|
5
|
+
import type { UserSession } from '../sessionManager/userSession.js';
|
|
6
|
+
import type { AllowanceRepository } from './repository.js';
|
|
7
|
+
export type AllowanceErrorReason = 'NoSession' | 'Rejected' | 'NotAvailable' | 'UnexpectedResponse';
|
|
8
|
+
export declare class AllowanceError extends Error {
|
|
9
|
+
readonly reason: AllowanceErrorReason;
|
|
10
|
+
constructor(reason: AllowanceErrorReason, message?: string);
|
|
11
|
+
}
|
|
12
|
+
type SessionsView = {
|
|
13
|
+
read(): UserSession[];
|
|
14
|
+
subscribe(callback: Callback<UserSession[]>): VoidFunction;
|
|
15
|
+
};
|
|
16
|
+
export type AllowanceService = {
|
|
17
|
+
getBulletinSigner(sessionId: string, productId: string): ResultAsync<PolkadotSigner, AllowanceError>;
|
|
18
|
+
getStatementStoreProver(sessionId: string, productId: string): ResultAsync<StatementProver, AllowanceError>;
|
|
19
|
+
};
|
|
20
|
+
export declare function createAllowanceService({ sessions, repository, }: {
|
|
21
|
+
sessions: SessionsView;
|
|
22
|
+
repository: AllowanceRepository;
|
|
23
|
+
}): AllowanceService;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createSr25519Prover, deriveSlotAccountPublicKey, ensureSubstrateSlotSr25519Ready, signSlotAccountSecret, } from '@novasamatech/statement-store';
|
|
2
|
+
import { ResultAsync, errAsync, okAsync } from 'neverthrow';
|
|
3
|
+
import { getPolkadotSigner } from 'polkadot-api/signer';
|
|
4
|
+
export class AllowanceError extends Error {
|
|
5
|
+
reason;
|
|
6
|
+
constructor(reason, message) {
|
|
7
|
+
super(message ?? reason);
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
this.name = 'AllowanceError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function createAllowanceService({ sessions, repository, }) {
|
|
13
|
+
const fetchKey = (sessionId, productId, resource) => {
|
|
14
|
+
return repository
|
|
15
|
+
.read(sessionId, productId, resource)
|
|
16
|
+
.mapErr(e => new AllowanceError('UnexpectedResponse', e.message))
|
|
17
|
+
.andThen(cached => (cached ? okAsync(cached) : requestFromMobile(sessionId, productId, resource)));
|
|
18
|
+
};
|
|
19
|
+
const requestFromMobile = (sessionId, productId, resource) => {
|
|
20
|
+
const session = sessions.read().find(s => s.id === sessionId);
|
|
21
|
+
if (!session) {
|
|
22
|
+
return errAsync(new AllowanceError('NoSession', `No active session ${sessionId}`));
|
|
23
|
+
}
|
|
24
|
+
return session
|
|
25
|
+
.requestResourceAllocation({
|
|
26
|
+
callingProductId: productId,
|
|
27
|
+
resources: [toApResource(resource)],
|
|
28
|
+
onExisting: 'Ignore',
|
|
29
|
+
})
|
|
30
|
+
.mapErr(e => new AllowanceError('UnexpectedResponse', e.message))
|
|
31
|
+
.andThen(outcomes => {
|
|
32
|
+
const outcome = outcomes[0];
|
|
33
|
+
if (!outcome) {
|
|
34
|
+
return errAsync(new AllowanceError('UnexpectedResponse', 'Empty allocation response'));
|
|
35
|
+
}
|
|
36
|
+
if (outcome.tag === 'Rejected') {
|
|
37
|
+
return errAsync(new AllowanceError('Rejected', `Allowance request rejected for ${resource}`));
|
|
38
|
+
}
|
|
39
|
+
if (outcome.tag === 'NotAvailable') {
|
|
40
|
+
return errAsync(new AllowanceError('NotAvailable', `Allowance not available for ${resource}`));
|
|
41
|
+
}
|
|
42
|
+
const allocated = outcome.value;
|
|
43
|
+
const expectedTag = resource === 'bulletin' ? 'BulletInAllowance' : 'StatementStoreAllowance';
|
|
44
|
+
if (allocated.tag !== expectedTag) {
|
|
45
|
+
return errAsync(new AllowanceError('UnexpectedResponse', `Expected ${expectedTag}, got ${allocated.tag}`));
|
|
46
|
+
}
|
|
47
|
+
const slotAccountKey = allocated.value.slotAccountKey;
|
|
48
|
+
return repository
|
|
49
|
+
.write(sessionId, productId, resource, slotAccountKey)
|
|
50
|
+
.mapErr(e => new AllowanceError('UnexpectedResponse', e.message))
|
|
51
|
+
.map(() => slotAccountKey);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
getBulletinSigner(sessionId, productId) {
|
|
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
|
+
},
|
|
58
|
+
getStatementStoreProver(sessionId, productId) {
|
|
59
|
+
return fetchKey(sessionId, productId, 'statementStore').map(secret => createSr25519Prover(secret));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function toApResource(resource) {
|
|
64
|
+
switch (resource) {
|
|
65
|
+
case 'bulletin':
|
|
66
|
+
return { tag: 'BulletInAllowance', value: undefined };
|
|
67
|
+
case 'statementStore':
|
|
68
|
+
return { tag: 'StatementStoreAllowance', value: undefined };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { AllowanceRepository, AllowanceResourceKind } from './repository.js';
|
|
2
|
+
export { createAllowanceRepository } from './repository.js';
|
|
3
|
+
export type { AllowanceErrorReason, AllowanceService } from './impl.js';
|
|
4
|
+
export { AllowanceError, createAllowanceService } from './impl.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StorageAdapter } from '@novasamatech/storage-adapter';
|
|
2
|
+
import type { ResultAsync } from 'neverthrow';
|
|
3
|
+
export type AllowanceResourceKind = 'bulletin' | 'statementStore';
|
|
4
|
+
export type AllowanceRepository = ReturnType<typeof createAllowanceRepository>;
|
|
5
|
+
export declare function createAllowanceRepository(salt: string, storage: StorageAdapter): {
|
|
6
|
+
read(sessionId: string, productId: string, resource: AllowanceResourceKind): ResultAsync<Uint8Array | null, Error>;
|
|
7
|
+
write(sessionId: string, productId: string, resource: AllowanceResourceKind, slotAccountKey: Uint8Array): ResultAsync<void, Error>;
|
|
8
|
+
clearSession(sessionId: string): ResultAsync<void, Error>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { gcm } from '@noble/ciphers/aes.js';
|
|
2
|
+
import { blake2b } from '@noble/hashes/blake2.js';
|
|
3
|
+
import { fromThrowable } from 'neverthrow';
|
|
4
|
+
import { fromHex, toHex } from 'polkadot-api/utils';
|
|
5
|
+
import { Bytes, Enum, Struct, Vector, _void, str } from 'scale-ts';
|
|
6
|
+
import { stringToBytes } from '../../crypto.js';
|
|
7
|
+
import { toError } from '../../helpers/utils.js';
|
|
8
|
+
const AllowanceResourceKindCodec = Enum({
|
|
9
|
+
bulletin: _void,
|
|
10
|
+
statementStore: _void,
|
|
11
|
+
});
|
|
12
|
+
const StoredAllowanceEntryCodec = Struct({
|
|
13
|
+
productId: str,
|
|
14
|
+
resource: AllowanceResourceKindCodec,
|
|
15
|
+
slotAccountKey: Bytes(),
|
|
16
|
+
});
|
|
17
|
+
const StoredAllowancesCodec = Vector(StoredAllowanceEntryCodec);
|
|
18
|
+
export function createAllowanceRepository(salt, storage) {
|
|
19
|
+
const baseKey = 'AllowanceKeys';
|
|
20
|
+
const encode = fromThrowable(StoredAllowancesCodec.enc, toError);
|
|
21
|
+
const decode = fromThrowable((value) => (value ? StoredAllowancesCodec.dec(value) : []), toError);
|
|
22
|
+
const encrypt = fromThrowable((value) => {
|
|
23
|
+
const aes = getAes(salt);
|
|
24
|
+
return toHex(aes.encrypt(value));
|
|
25
|
+
}, toError);
|
|
26
|
+
const decrypt = fromThrowable((value) => {
|
|
27
|
+
if (value === null)
|
|
28
|
+
return null;
|
|
29
|
+
const aes = getAes(salt);
|
|
30
|
+
return aes.decrypt(fromHex(value));
|
|
31
|
+
}, toError);
|
|
32
|
+
const readAll = (sessionId) => storage.read(createKey(baseKey, sessionId)).andThen(decrypt).andThen(decode);
|
|
33
|
+
const writeAll = (sessionId, entries) => encode(entries)
|
|
34
|
+
.andThen(encrypt)
|
|
35
|
+
.asyncAndThen(value => storage.write(createKey(baseKey, sessionId), value));
|
|
36
|
+
return {
|
|
37
|
+
read(sessionId, productId, resource) {
|
|
38
|
+
return readAll(sessionId).map(entries => {
|
|
39
|
+
const entry = entries.find(e => e.productId === productId && e.resource.tag === resource);
|
|
40
|
+
return entry ? entry.slotAccountKey : null;
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
write(sessionId, productId, resource, slotAccountKey) {
|
|
44
|
+
return readAll(sessionId).andThen(entries => {
|
|
45
|
+
const next = [
|
|
46
|
+
...entries.filter(e => !(e.productId === productId && e.resource.tag === resource)),
|
|
47
|
+
{ productId, resource: { tag: resource, value: undefined }, slotAccountKey },
|
|
48
|
+
];
|
|
49
|
+
return writeAll(sessionId, next);
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
clearSession(sessionId) {
|
|
53
|
+
return storage.clear(createKey(baseKey, sessionId));
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const createKey = (key, context) => `${key}_${context}`;
|
|
58
|
+
function getAes(salt) {
|
|
59
|
+
return gcm(blake2b(stringToBytes(salt), { dkLen: 16 }), blake2b(stringToBytes('nonce'), { dkLen: 32 }));
|
|
60
|
+
}
|
|
@@ -215,6 +215,9 @@ export type EncryptedHandshakeResponseV2Value = {
|
|
|
215
215
|
* matches the peer's wire format directly.
|
|
216
216
|
*/
|
|
217
217
|
export declare const EncryptedHandshakeResponseV2: import("scale-ts").Codec<{
|
|
218
|
+
tag: "Failed";
|
|
219
|
+
value: string;
|
|
220
|
+
} | {
|
|
218
221
|
tag: "Pending";
|
|
219
222
|
value: {
|
|
220
223
|
tag: "AllowanceAllocation";
|
|
@@ -228,9 +231,6 @@ export declare const EncryptedHandshakeResponseV2: import("scale-ts").Codec<{
|
|
|
228
231
|
identityChatPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
229
232
|
deviceEncPubKey: Uint8Array<ArrayBufferLike>;
|
|
230
233
|
};
|
|
231
|
-
} | {
|
|
232
|
-
tag: "Failed";
|
|
233
|
-
value: string;
|
|
234
234
|
}>;
|
|
235
235
|
export declare const HandshakeResponseV2: import("scale-ts").Codec<{
|
|
236
236
|
encrypted: Uint8Array<ArrayBufferLike>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { StatementStoreAdapter } from '@novasamatech/statement-store';
|
|
2
2
|
import type { StorageAdapter } from '@novasamatech/storage-adapter';
|
|
3
3
|
import type { Callback } from '../../types.js';
|
|
4
|
+
import type { AllowanceRepository } from '../allowance/index.js';
|
|
4
5
|
import type { UserSecretRepository } from '../userSecretRepository.js';
|
|
5
6
|
import type { StoredUserSession, UserSessionRepository } from '../userSessionRepository.js';
|
|
6
7
|
import type { UserSession } from './userSession.js';
|
|
@@ -10,8 +11,9 @@ type Params = {
|
|
|
10
11
|
statementStore: StatementStoreAdapter;
|
|
11
12
|
ssoSessionRepository: UserSessionRepository;
|
|
12
13
|
userSecretRepository: UserSecretRepository;
|
|
14
|
+
allowanceRepository: AllowanceRepository;
|
|
13
15
|
};
|
|
14
|
-
export declare function createSsoSessionManager({ ssoSessionRepository, userSecretRepository, statementStore, storage, }: Params): {
|
|
16
|
+
export declare function createSsoSessionManager({ ssoSessionRepository, userSecretRepository, allowanceRepository, statementStore, storage, }: Params): {
|
|
15
17
|
sessions: {
|
|
16
18
|
read: () => UserSession[];
|
|
17
19
|
subscribe: (callback: Callback<UserSession[]>) => () => void;
|
|
@@ -4,7 +4,7 @@ import { emitHostPappDebugMessage } from '../../debugBus.js';
|
|
|
4
4
|
import { createState } from '../../helpers/state.js';
|
|
5
5
|
import { createSsoStatementProver } from '../ssoSessionProver.js';
|
|
6
6
|
import { createUserSession } from './userSession.js';
|
|
7
|
-
export function createSsoSessionManager({ ssoSessionRepository, userSecretRepository, statementStore, storage, }) {
|
|
7
|
+
export function createSsoSessionManager({ ssoSessionRepository, userSecretRepository, allowanceRepository, statementStore, storage, }) {
|
|
8
8
|
const localSessions = createState({});
|
|
9
9
|
const sessionUnsubscribes = new Map();
|
|
10
10
|
const releaseSession = (id) => {
|
|
@@ -77,7 +77,8 @@ export function createSsoSessionManager({ ssoSessionRepository, userSecretReposi
|
|
|
77
77
|
return session
|
|
78
78
|
.sendDisconnectMessage()
|
|
79
79
|
.andThen(() => disconnect(userSession))
|
|
80
|
-
.andThen(() => userSecretRepository.clear(userSession.id))
|
|
80
|
+
.andThen(() => userSecretRepository.clear(userSession.id))
|
|
81
|
+
.andThen(() => allowanceRepository.clearSession(userSession.id));
|
|
81
82
|
},
|
|
82
83
|
dispose() {
|
|
83
84
|
for (const session of Object.values(localSessions.read())) {
|
|
@@ -73,15 +73,15 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
73
73
|
resources: ({
|
|
74
74
|
tag: "StatementStoreAllowance";
|
|
75
75
|
value: undefined;
|
|
76
|
-
} | {
|
|
77
|
-
tag: "BulletInAllowance";
|
|
78
|
-
value: undefined;
|
|
79
76
|
} | {
|
|
80
77
|
tag: "SmartContractAllowance";
|
|
81
78
|
value: number;
|
|
82
79
|
} | {
|
|
83
80
|
tag: "AutoSigning";
|
|
84
81
|
value: undefined;
|
|
82
|
+
} | {
|
|
83
|
+
tag: "BulletInAllowance";
|
|
84
|
+
value: undefined;
|
|
85
85
|
})[];
|
|
86
86
|
onExisting: "Ignore" | "Increase";
|
|
87
87
|
};
|
|
@@ -99,11 +99,6 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
99
99
|
value: {
|
|
100
100
|
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
101
101
|
};
|
|
102
|
-
} | {
|
|
103
|
-
tag: "BulletInAllowance";
|
|
104
|
-
value: {
|
|
105
|
-
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
106
|
-
};
|
|
107
102
|
} | {
|
|
108
103
|
tag: "SmartContractAllowance";
|
|
109
104
|
value: undefined;
|
|
@@ -113,6 +108,11 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
113
108
|
productDerivationSecret: string;
|
|
114
109
|
productRootPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
115
110
|
};
|
|
111
|
+
} | {
|
|
112
|
+
tag: "BulletInAllowance";
|
|
113
|
+
value: {
|
|
114
|
+
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
115
|
+
};
|
|
116
116
|
};
|
|
117
117
|
} | {
|
|
118
118
|
tag: "NotAvailable";
|
|
@@ -3,15 +3,15 @@ export type ApAllocatableResource = CodecType<typeof ApAllocatableResourceCodec>
|
|
|
3
3
|
export declare const ApAllocatableResourceCodec: import("scale-ts").Codec<{
|
|
4
4
|
tag: "StatementStoreAllowance";
|
|
5
5
|
value: undefined;
|
|
6
|
-
} | {
|
|
7
|
-
tag: "BulletInAllowance";
|
|
8
|
-
value: undefined;
|
|
9
6
|
} | {
|
|
10
7
|
tag: "SmartContractAllowance";
|
|
11
8
|
value: number;
|
|
12
9
|
} | {
|
|
13
10
|
tag: "AutoSigning";
|
|
14
11
|
value: undefined;
|
|
12
|
+
} | {
|
|
13
|
+
tag: "BulletInAllowance";
|
|
14
|
+
value: undefined;
|
|
15
15
|
}>;
|
|
16
16
|
export type ApAllocatedResource = CodecType<typeof ApAllocatedResourceCodec>;
|
|
17
17
|
export declare const ApAllocatedResourceCodec: import("scale-ts").Codec<{
|
|
@@ -19,11 +19,6 @@ export declare const ApAllocatedResourceCodec: import("scale-ts").Codec<{
|
|
|
19
19
|
value: {
|
|
20
20
|
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
21
21
|
};
|
|
22
|
-
} | {
|
|
23
|
-
tag: "BulletInAllowance";
|
|
24
|
-
value: {
|
|
25
|
-
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
26
|
-
};
|
|
27
22
|
} | {
|
|
28
23
|
tag: "SmartContractAllowance";
|
|
29
24
|
value: undefined;
|
|
@@ -33,6 +28,11 @@ export declare const ApAllocatedResourceCodec: import("scale-ts").Codec<{
|
|
|
33
28
|
productDerivationSecret: string;
|
|
34
29
|
productRootPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
35
30
|
};
|
|
31
|
+
} | {
|
|
32
|
+
tag: "BulletInAllowance";
|
|
33
|
+
value: {
|
|
34
|
+
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
35
|
+
};
|
|
36
36
|
}>;
|
|
37
37
|
export type ApAllocationOutcome = CodecType<typeof ApAllocationOutcomeCodec>;
|
|
38
38
|
export declare const ApAllocationOutcomeCodec: import("scale-ts").Codec<{
|
|
@@ -45,11 +45,6 @@ export declare const ApAllocationOutcomeCodec: import("scale-ts").Codec<{
|
|
|
45
45
|
value: {
|
|
46
46
|
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
47
47
|
};
|
|
48
|
-
} | {
|
|
49
|
-
tag: "BulletInAllowance";
|
|
50
|
-
value: {
|
|
51
|
-
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
52
|
-
};
|
|
53
48
|
} | {
|
|
54
49
|
tag: "SmartContractAllowance";
|
|
55
50
|
value: undefined;
|
|
@@ -59,6 +54,11 @@ export declare const ApAllocationOutcomeCodec: import("scale-ts").Codec<{
|
|
|
59
54
|
productDerivationSecret: string;
|
|
60
55
|
productRootPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
61
56
|
};
|
|
57
|
+
} | {
|
|
58
|
+
tag: "BulletInAllowance";
|
|
59
|
+
value: {
|
|
60
|
+
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
61
|
+
};
|
|
62
62
|
};
|
|
63
63
|
} | {
|
|
64
64
|
tag: "NotAvailable";
|
|
@@ -71,15 +71,15 @@ export declare const ResourceAllocationRequestCodec: import("scale-ts").Codec<{
|
|
|
71
71
|
resources: ({
|
|
72
72
|
tag: "StatementStoreAllowance";
|
|
73
73
|
value: undefined;
|
|
74
|
-
} | {
|
|
75
|
-
tag: "BulletInAllowance";
|
|
76
|
-
value: undefined;
|
|
77
74
|
} | {
|
|
78
75
|
tag: "SmartContractAllowance";
|
|
79
76
|
value: number;
|
|
80
77
|
} | {
|
|
81
78
|
tag: "AutoSigning";
|
|
82
79
|
value: undefined;
|
|
80
|
+
} | {
|
|
81
|
+
tag: "BulletInAllowance";
|
|
82
|
+
value: undefined;
|
|
83
83
|
})[];
|
|
84
84
|
onExisting: "Ignore" | "Increase";
|
|
85
85
|
}>;
|
|
@@ -96,11 +96,6 @@ export declare const ResourceAllocationResponseCodec: import("scale-ts").Codec<{
|
|
|
96
96
|
value: {
|
|
97
97
|
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
98
98
|
};
|
|
99
|
-
} | {
|
|
100
|
-
tag: "BulletInAllowance";
|
|
101
|
-
value: {
|
|
102
|
-
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
103
|
-
};
|
|
104
99
|
} | {
|
|
105
100
|
tag: "SmartContractAllowance";
|
|
106
101
|
value: undefined;
|
|
@@ -110,6 +105,11 @@ export declare const ResourceAllocationResponseCodec: import("scale-ts").Codec<{
|
|
|
110
105
|
productDerivationSecret: string;
|
|
111
106
|
productRootPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
112
107
|
};
|
|
108
|
+
} | {
|
|
109
|
+
tag: "BulletInAllowance";
|
|
110
|
+
value: {
|
|
111
|
+
slotAccountKey: Uint8Array<ArrayBufferLike>;
|
|
112
|
+
};
|
|
113
113
|
};
|
|
114
114
|
} | {
|
|
115
115
|
tag: "NotAvailable";
|
|
@@ -11,6 +11,7 @@ import type { ApAllocationOutcome, ResourceAllocationRequest } from './scale/res
|
|
|
11
11
|
import type { SigningPayloadRequest, SigningPayloadResponseData, SigningRawRequest } from './scale/signing.js';
|
|
12
12
|
export type UserSession = StoredUserSession & {
|
|
13
13
|
sendDisconnectMessage(): ResultAsync<void, Error>;
|
|
14
|
+
abortPendingRequests(): ResultAsync<void, Error>;
|
|
14
15
|
signPayload(payload: SigningPayloadRequest): ResultAsync<SigningPayloadResponseData, Error>;
|
|
15
16
|
signRaw(payload: SigningRawRequest): ResultAsync<SigningPayloadResponseData, Error>;
|
|
16
17
|
createTransaction(payload: CreateTransactionRequest): ResultAsync<Uint8Array, Error>;
|
|
@@ -13,6 +13,8 @@ import { RemoteMessageCodec } from './scale/remoteMessage.js';
|
|
|
13
13
|
// payload is for an SDK version the mobile app doesn't support yet. After
|
|
14
14
|
// this timeout the queue task fails, freeing the pool for the next request.
|
|
15
15
|
const QUEUE_TASK_TIMEOUT_MS = 180_000;
|
|
16
|
+
// Mobile SSO statements allow 256 KiB total; keep headroom for statement/session overhead.
|
|
17
|
+
const MAX_SSO_REQUEST_SIZE = 254 * 1024;
|
|
16
18
|
function withQueueTimeout(resultAsync, label) {
|
|
17
19
|
const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(err(new Error(`${label} timed out — queue freed`))), QUEUE_TASK_TIMEOUT_MS));
|
|
18
20
|
return ResultAsync.fromPromise(Promise.race([resultAsync, timeoutPromise]), toError).andThen(r => r);
|
|
@@ -64,12 +66,20 @@ function withHostActionTrace(result, messageId, sessionId) {
|
|
|
64
66
|
}
|
|
65
67
|
export function createUserSession({ userSession, statementStore, encryption, storage, prover, }) {
|
|
66
68
|
const requestQueue = createAsyncTaskPool({ poolSize: 1, retryCount: 0, retryDelay: 0 });
|
|
69
|
+
// Shared abort handle for everything currently on the request queue.
|
|
70
|
+
// abortPendingRequests() fires it to drop the in-flight task plus anything
|
|
71
|
+
// queued behind it, then swaps in a fresh controller so later requests aren't
|
|
72
|
+
// pre-aborted.
|
|
73
|
+
let requestAbort = new AbortController();
|
|
74
|
+
// Enqueue against the live abort signal so abortPendingRequests() can drop every pending task.
|
|
75
|
+
const enqueue = (fn) => requestQueue.call(fn, { signal: requestAbort.signal });
|
|
67
76
|
const session = createSession({
|
|
68
77
|
localAccount: userSession.localAccount,
|
|
69
78
|
remoteAccount: userSession.remoteAccount,
|
|
70
79
|
statementStore,
|
|
71
80
|
encryption,
|
|
72
81
|
prover,
|
|
82
|
+
maxRequestSize: MAX_SSO_REQUEST_SIZE,
|
|
73
83
|
});
|
|
74
84
|
const processedMessages = fieldListView({
|
|
75
85
|
storage,
|
|
@@ -80,7 +90,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
80
90
|
return {
|
|
81
91
|
...userSession,
|
|
82
92
|
signPayload(payload) {
|
|
83
|
-
return
|
|
93
|
+
return enqueue(() => {
|
|
84
94
|
const messageId = nanoid();
|
|
85
95
|
const data = enumValue('v1', enumValue('SignRequest', enumValue('Payload', payload)));
|
|
86
96
|
emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
|
|
@@ -106,7 +116,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
106
116
|
});
|
|
107
117
|
},
|
|
108
118
|
signRaw(payload) {
|
|
109
|
-
return
|
|
119
|
+
return enqueue(() => {
|
|
110
120
|
const messageId = nanoid();
|
|
111
121
|
const data = enumValue('v1', enumValue('SignRequest', enumValue('Raw', payload)));
|
|
112
122
|
emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
|
|
@@ -132,7 +142,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
132
142
|
});
|
|
133
143
|
},
|
|
134
144
|
createTransaction(payload) {
|
|
135
|
-
return
|
|
145
|
+
return enqueue(() => {
|
|
136
146
|
const messageId = nanoid();
|
|
137
147
|
const request = session.request(RemoteMessageCodec, {
|
|
138
148
|
messageId,
|
|
@@ -159,7 +169,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
159
169
|
});
|
|
160
170
|
},
|
|
161
171
|
sendDisconnectMessage() {
|
|
162
|
-
return
|
|
172
|
+
return enqueue(() => session
|
|
163
173
|
.submitRequestMessage(RemoteMessageCodec, {
|
|
164
174
|
messageId: nanoid(),
|
|
165
175
|
data: enumValue('v1', enumValue('Disconnected', undefined)),
|
|
@@ -167,7 +177,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
167
177
|
.map(() => undefined));
|
|
168
178
|
},
|
|
169
179
|
getRingVrfAlias(productAccountId, productId) {
|
|
170
|
-
return
|
|
180
|
+
return enqueue(() => {
|
|
171
181
|
const messageId = nanoid();
|
|
172
182
|
const data = enumValue('v1', enumValue('RingVrfAliasRequest', {
|
|
173
183
|
productAccountId,
|
|
@@ -188,7 +198,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
188
198
|
});
|
|
189
199
|
},
|
|
190
200
|
requestResourceAllocation(request) {
|
|
191
|
-
return
|
|
201
|
+
return enqueue(() => {
|
|
192
202
|
const messageId = nanoid();
|
|
193
203
|
const sendRequest = session.request(RemoteMessageCodec, {
|
|
194
204
|
messageId,
|
|
@@ -268,6 +278,17 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
268
278
|
});
|
|
269
279
|
});
|
|
270
280
|
},
|
|
281
|
+
abortPendingRequests() {
|
|
282
|
+
// Drop the whole request queue: aborting the shared signal rejects the
|
|
283
|
+
// in-flight task and every request queued behind it, freeing the single
|
|
284
|
+
// slot immediately instead of waiting out the per-task 180s timeout. Swap
|
|
285
|
+
// in a fresh controller so subsequent requests aren't pre-aborted.
|
|
286
|
+
requestAbort.abort(new Error('Session request aborted'));
|
|
287
|
+
requestAbort = new AbortController();
|
|
288
|
+
// Then supersede the in-flight on-chain batch with an empty one and reject
|
|
289
|
+
// any session-level response waiters left orphaned by the dropped tasks.
|
|
290
|
+
return session.clearOutgoingStatement();
|
|
291
|
+
},
|
|
271
292
|
dispose() {
|
|
272
293
|
return session.dispose();
|
|
273
294
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novasamatech/host-papp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.1",
|
|
5
5
|
"description": "Polkadot app integration",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -34,19 +34,18 @@
|
|
|
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.
|
|
38
|
-
"@novasamatech/scale": "0.8.
|
|
39
|
-
"@novasamatech/statement-store": "0.8.
|
|
40
|
-
"@novasamatech/storage-adapter": "0.8.
|
|
37
|
+
"@novasamatech/host-api": "0.8.1",
|
|
38
|
+
"@novasamatech/scale": "0.8.1",
|
|
39
|
+
"@novasamatech/statement-store": "0.8.1",
|
|
40
|
+
"@novasamatech/storage-adapter": "0.8.1",
|
|
41
41
|
"@polkadot-api/utils": "^0.4.0",
|
|
42
42
|
"@polkadot-labs/hdkd-helpers": "^0.0.30",
|
|
43
43
|
"nanoevents": "9.1.0",
|
|
44
|
-
"nanoid": "5.1.
|
|
44
|
+
"nanoid": "5.1.11",
|
|
45
45
|
"neverthrow": "^8.2.0",
|
|
46
46
|
"polkadot-api": ">=2",
|
|
47
47
|
"rxjs": "^7.8.2",
|
|
48
|
-
"scale-ts": "1.6.1"
|
|
49
|
-
"verifiablejs": "1.2.0"
|
|
48
|
+
"scale-ts": "1.6.1"
|
|
50
49
|
},
|
|
51
50
|
"publishConfig": {
|
|
52
51
|
"access": "public"
|