@novasamatech/host-papp 0.9.3 → 0.9.4

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.
@@ -76,5 +76,14 @@ export declare function createAuth({ hostMetadata, deviceIdentity, deviceIdentit
76
76
  };
77
77
  authenticate(): ResultAsync<StoredUserSession | null, Error>;
78
78
  abortAuthentication(): void;
79
+ /**
80
+ * Discard the persisted device keypair and the processed-handshake marker
81
+ * stored with it, so the next pairing runs on a new topic with a new key —
82
+ * which makes any cached on-chain HandshakeSuccess undecryptable.
83
+ *
84
+ * No-op for hosts supplying their own `deviceIdentity` factory, and a
85
+ * pairing already in flight keeps the old identity.
86
+ */
87
+ resetDeviceIdentity(): ResultAsync<void, Error>;
79
88
  };
80
89
  export {};
@@ -146,6 +146,17 @@ export function createAuth({ hostMetadata, deviceIdentity, deviceIdentityStore,
146
146
  authResult = null;
147
147
  pairingStatus.reset();
148
148
  },
149
+ /**
150
+ * Discard the persisted device keypair and the processed-handshake marker
151
+ * stored with it, so the next pairing runs on a new topic with a new key —
152
+ * which makes any cached on-chain HandshakeSuccess undecryptable.
153
+ *
154
+ * No-op for hosts supplying their own `deviceIdentity` factory, and a
155
+ * pairing already in flight keeps the old identity.
156
+ */
157
+ resetDeviceIdentity() {
158
+ return deviceIdentityStore.reset();
159
+ },
149
160
  };
150
161
  function persistAndNotify(identity, success, _flowId) {
151
162
  const localAccount = createLocalSessionAccount(createAccountId(identity.statementAccountPublicKey));
@@ -9,6 +9,7 @@ export type DeviceIdentityStore = {
9
9
  loadOrCreate(): ResultAsync<DeviceIdentity, Error>;
10
10
  readLastProcessedHandshakeStatement(): ResultAsync<string | null, Error>;
11
11
  writeLastProcessedHandshakeStatement(hex: string): ResultAsync<void, Error>;
12
+ reset(): ResultAsync<void, Error>;
12
13
  };
13
14
  export declare function createDeviceIdentityStore(salt: string, storage: StorageAdapter): DeviceIdentityStore;
14
15
  export declare const awaitDeviceIdentity: (store: DeviceIdentityStore) => Promise<DeviceIdentity>;
@@ -66,6 +66,9 @@ export function createDeviceIdentityStore(salt, storage) {
66
66
  return write({ ...existing, lastProcessedHandshakeStatement: hex });
67
67
  });
68
68
  },
69
+ reset() {
70
+ return storage.clear(KEY);
71
+ },
69
72
  };
70
73
  }
71
74
  // Re-export the awaitable form for convenience, since most call sites already
@@ -1,5 +1,6 @@
1
1
  import type { StatementStoreAdapter } from '@novasamatech/statement-store';
2
2
  import type { StorageAdapter } from '@novasamatech/storage-adapter';
3
+ import { ResultAsync } from 'neverthrow';
3
4
  import type { IdentityRepository } from '../../identity/types.js';
4
5
  import type { Callback } from '../../types.js';
5
6
  import type { AllowanceRepository } from '../allowance/index.js';
@@ -20,7 +21,11 @@ export declare function createSsoSessionManager({ ssoSessionRepository, userSecr
20
21
  read: () => UserSession[];
21
22
  subscribe: (callback: Callback<UserSession[]>) => () => void;
22
23
  };
23
- disconnect(userSession: StoredUserSession): import("neverthrow").ResultAsync<void, Error>;
24
+ disconnect(userSession: StoredUserSession): ResultAsync<undefined, Error>;
25
+ /**
26
+ * Teardown by id alone, without notifying the peer.
27
+ * */
28
+ forget(sessionId: string): ResultAsync<undefined, Error>;
24
29
  dispose(): void;
25
30
  };
26
31
  export {};
@@ -1,9 +1,9 @@
1
1
  import { createEncryption } from '@novasamatech/statement-store';
2
- import { okAsync } from 'neverthrow';
2
+ import { ResultAsync, okAsync } from 'neverthrow';
3
3
  import { emitHostPappDebugMessage } from '../../debugBus.js';
4
4
  import { createState } from '../../helpers/state.js';
5
5
  import { createSsoStatementProver } from '../ssoSessionProver.js';
6
- import { createUserSession } from './userSession.js';
6
+ import { createUserSession, processedMessagesKey } from './userSession.js';
7
7
  export function createSsoSessionManager({ ssoSessionRepository, userSecretRepository, allowanceRepository, identityRepository, statementStore, storage, }) {
8
8
  const localSessions = createState({});
9
9
  const sessionUnsubscribes = new Map();
@@ -11,9 +11,16 @@ export function createSsoSessionManager({ ssoSessionRepository, userSecretReposi
11
11
  sessionUnsubscribes.get(id)?.();
12
12
  sessionUnsubscribes.delete(id);
13
13
  };
14
- const disconnect = (session) => {
15
- return ssoSessionRepository.filter(s => s.id !== session.id).map(() => undefined);
16
- };
14
+ // The session-list write goes first: it is what notifies the subscription
15
+ // above to unsubscribe and dispose the live session.
16
+ const purgeSession = (sessionId) => ssoSessionRepository
17
+ .filter(s => s.id !== sessionId)
18
+ .andThen(() => ResultAsync.combine([
19
+ userSecretRepository.clear(sessionId),
20
+ allowanceRepository.clearSession(sessionId),
21
+ storage.clear(processedMessagesKey(sessionId)),
22
+ ]))
23
+ .map(() => undefined);
17
24
  ssoSessionRepository.subscribe(userSessions => {
18
25
  const activeSessions = localSessions.read();
19
26
  const toRemove = new Set(Object.keys(activeSessions));
@@ -36,7 +43,7 @@ export function createSsoSessionManager({ ssoSessionRepository, userSecretReposi
36
43
  case 'v1': {
37
44
  switch (message.data.value.tag) {
38
45
  case 'Disconnected':
39
- return disconnect(userSession).map(() => true);
46
+ return purgeSession(userSession.id).map(() => false);
40
47
  }
41
48
  }
42
49
  }
@@ -76,9 +83,18 @@ export function createSsoSessionManager({ ssoSessionRepository, userSecretReposi
76
83
  const session = createSession(userSession, statementStore, storage, userSecretRepository, allowanceRepository, identityRepository);
77
84
  return session
78
85
  .sendDisconnectMessage()
79
- .andThen(() => disconnect(userSession))
80
- .andThen(() => userSecretRepository.clear(userSession.id))
81
- .andThen(() => allowanceRepository.clearSession(userSession.id));
86
+ .orElse(error => {
87
+ console.warn('[host-papp] disconnect: peer notification failed, tearing down locally anyway', error);
88
+ return okAsync(undefined);
89
+ })
90
+ .andTee(() => session.dispose())
91
+ .andThen(() => purgeSession(userSession.id));
92
+ },
93
+ /**
94
+ * Teardown by id alone, without notifying the peer.
95
+ * */
96
+ forget(sessionId) {
97
+ return purgeSession(sessionId);
82
98
  },
83
99
  dispose() {
84
100
  for (const session of Object.values(localSessions.read())) {
@@ -12,6 +12,7 @@ import { RemoteMessageCodec } from './scale/remoteMessage.js';
12
12
  import type { ApAllocationOutcome, ResourceAllocationRequest } from './scale/resourceAllocation.js';
13
13
  import type { SignVrfRequest } from './scale/signVrf.js';
14
14
  import type { SignRawLegacyRequest, SigningPayloadRequest, SigningPayloadResponseData, SigningRawRequest } from './scale/signing.js';
15
+ export declare const processedMessagesKey: (sessionId: string) => string;
15
16
  export type UserSession = StoredUserSession & {
16
17
  /** Read this session's persisted allowance slot-account key for a product/resource. */
17
18
  readAllowance(productId: string, resource: AllowanceResourceKind): ResultAsync<Uint8Array | null, Error>;
@@ -16,6 +16,8 @@ import { RemoteMessageCodec } from './scale/remoteMessage.js';
16
16
  const QUEUE_TASK_TIMEOUT_MS = 240_000;
17
17
  // Mobile SSO statements allow 500 KiB total; keep headroom for statement/session overhead.
18
18
  const MAX_SSO_REQUEST_SIZE = 498 * 1024;
19
+ // Per-session dedup log of already-handled peer message ids.
20
+ export const processedMessagesKey = (sessionId) => `sso_processed_${sessionId}`;
19
21
  function withQueueTimeout(resultAsync, label) {
20
22
  const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(err(new Error(`${label} timed out — queue freed`))), QUEUE_TASK_TIMEOUT_MS));
21
23
  return ResultAsync.fromPromise(Promise.race([resultAsync, timeoutPromise]), toError).andThen(r => r);
@@ -106,7 +108,7 @@ export function createUserSession({ userSession, statementStore, encryption, sto
106
108
  });
107
109
  const processedMessages = fieldListView({
108
110
  storage,
109
- key: `sso_processed_${userSession.id}`,
111
+ key: processedMessagesKey(userSession.id),
110
112
  from: JSON.parse,
111
113
  to: JSON.stringify,
112
114
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/host-papp",
3
3
  "type": "module",
4
- "version": "0.9.3",
4
+ "version": "0.9.4",
5
5
  "description": "Polkadot app integration",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -38,10 +38,10 @@
38
38
  "@noble/ciphers": "2.2.0",
39
39
  "@noble/curves": "2.2.0",
40
40
  "@noble/hashes": "2.2.0",
41
- "@novasamatech/host-api": "0.9.3",
42
- "@novasamatech/scale": "0.9.3",
43
- "@novasamatech/statement-store": "0.9.3",
44
- "@novasamatech/storage-adapter": "0.9.3",
41
+ "@novasamatech/host-api": "0.9.4",
42
+ "@novasamatech/scale": "0.9.4",
43
+ "@novasamatech/statement-store": "0.9.4",
44
+ "@novasamatech/storage-adapter": "0.9.4",
45
45
  "@polkadot-api/utils": "^0.4.0",
46
46
  "@polkadot-labs/hdkd-helpers": "^0.0.31",
47
47
  "nanoevents": "10.0.0",