@novasamatech/host-papp 0.9.2 → 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.
- package/README.md +62 -15
- package/dist/index.d.ts +2 -1
- package/dist/sso/auth/impl.d.ts +9 -0
- package/dist/sso/auth/impl.js +11 -0
- package/dist/sso/deviceIdentityStore.d.ts +1 -0
- package/dist/sso/deviceIdentityStore.js +3 -0
- package/dist/sso/sessionManager/impl.d.ts +6 -1
- package/dist/sso/sessionManager/impl.js +25 -9
- package/dist/sso/sessionManager/scale/remoteMessage.d.ts +102 -2
- package/dist/sso/sessionManager/scale/remoteMessage.js +11 -1
- package/dist/sso/sessionManager/scale/resourceAllocation.d.ts +3 -0
- package/dist/sso/sessionManager/scale/resourceAllocation.js +11 -0
- package/dist/sso/sessionManager/scale/ringVrf.d.ts +43 -2
- package/dist/sso/sessionManager/scale/ringVrf.js +12 -1
- package/dist/sso/sessionManager/scale/ringVrfKeys.d.ts +85 -0
- package/dist/sso/sessionManager/scale/ringVrfKeys.js +22 -0
- package/dist/sso/sessionManager/userSession.d.ts +23 -3
- package/dist/sso/sessionManager/userSession.js +62 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -218,34 +218,74 @@ vrf.match(
|
|
|
218
218
|
This is the non-`AutoSigning` path only: when `AutoSigning` covers the account the host signs locally and never
|
|
219
219
|
round-trips to the wallet.
|
|
220
220
|
|
|
221
|
-
## Ring VRF proofs and aliases
|
|
221
|
+
## Ring VRF keys, proofs and aliases
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
Ring VRF member keys are **explicit and product-owned** (RFC-0024): a product registers the keys it owns against the
|
|
224
|
+
rings it intends them for, other products discover those registrations by an anonymized handle, and the handle is passed
|
|
225
|
+
to every call that uses the key. The paired device is the authoritative registry — it needs the complete set to serve
|
|
226
|
+
slot assignment and PGAS claims, and to show the user what their keys are used for.
|
|
227
227
|
|
|
228
228
|
```ts
|
|
229
|
-
// [productId, suffix]. The suffix is the wire `Index(u32) | Raw([u8; 32])` selector
|
|
230
|
-
// (RFC 0022): `Index` for a plain index, `Raw` for a raw 32-byte index. It
|
|
231
|
-
// expands to the same 32-byte value as a product account's derivation index.
|
|
232
|
-
const context = ['product.dot', { tag: 'Index', value: 0 }];
|
|
233
229
|
const ring = {
|
|
234
230
|
chainId: '0x…', // 32-byte chain genesis hash
|
|
235
231
|
junctions: [{ tag: 'PalletInstance', value: 42 }],
|
|
236
232
|
};
|
|
237
233
|
|
|
238
|
-
|
|
234
|
+
// Register a key owned by `peopl.dot` at index 0 for that ring. Consent-free and
|
|
235
|
+
// idempotent — re-registering an index for another ring extends the entry.
|
|
236
|
+
const publicKey = await currentSession.registerRingVrfKey('peopl.dot', { tag: 'Index', value: 0 }, ring);
|
|
237
|
+
|
|
238
|
+
// Discover another product's keys. `'PublicKey'` disclosure additionally returns
|
|
239
|
+
// the member public key, which is linkable across every ring it appears in and so
|
|
240
|
+
// is permissioned cross-product.
|
|
241
|
+
const entries = await currentSession.listRingVrfKeys('game.dot', 'peopl.dot', 'Anonymized');
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A `UserSession` can then ask the paired device for a privacy-preserving contextual alias, or a ring VRF proof, for a
|
|
245
|
+
given `keyHandle`, product-scoped `context` and `ring` location. `callingProductId` names the product the host is acting
|
|
246
|
+
for — it is what the owner's allowlist is checked against when the handle is foreign. Both take the same
|
|
247
|
+
`(keyHandle, context, ring)` so the alias in the proof matches `getRingVrfAlias`.
|
|
239
248
|
|
|
240
|
-
|
|
249
|
+
```ts
|
|
250
|
+
// The key handle names a slot in the owner's ring VRF domain. Select it from
|
|
251
|
+
// `listRingVrfKeys` by declared ring and pass it through opaquely — never
|
|
252
|
+
// hardcode another product's index.
|
|
253
|
+
const keyHandle = ['peopl.dot', { tag: 'Index', value: 0 }];
|
|
254
|
+
|
|
255
|
+
// [productId, suffix]. The suffix is the wire `Index(u32) | Raw([u8; 32])` selector
|
|
256
|
+
// (RFC 0022): `Index` for a plain index, `Raw` for a raw 32-byte index. It
|
|
257
|
+
// expands to the same 32-byte value as a product account's derivation index.
|
|
258
|
+
const context = ['product.dot', { tag: 'Index', value: 0 }];
|
|
259
|
+
|
|
260
|
+
const alias = await currentSession.getRingVrfAlias('caller.dot', keyHandle, context, ring);
|
|
261
|
+
|
|
262
|
+
const proof = await currentSession.createRingVrfProof(
|
|
263
|
+
'caller.dot',
|
|
264
|
+
keyHandle,
|
|
265
|
+
context,
|
|
266
|
+
ring,
|
|
267
|
+
new Uint8Array([0x48, 0x69]),
|
|
268
|
+
);
|
|
241
269
|
proof.match(
|
|
242
270
|
({ proof, contextualAlias, ringIndex, ringRevision }) =>
|
|
243
271
|
console.log('proof at ring', ringIndex, 'revision', ringRevision),
|
|
244
|
-
// failures decode to a structured `
|
|
272
|
+
// failures decode to a structured `CreateProofErr` — RingNotFound / NotMember /
|
|
273
|
+
// KeyNotRegistered / KeyNotInRing / NotAllowlisted / Rejected / Unknown
|
|
245
274
|
error => console.error('proof failed:', error),
|
|
246
275
|
);
|
|
276
|
+
|
|
277
|
+
// `ringVrfSign` signs with the member key itself instead of proving membership
|
|
278
|
+
// anonymously. No context and no ring — nothing for either to scope — and the
|
|
279
|
+
// result is verified against the member public key, so it is linkable to every
|
|
280
|
+
// other use of that key.
|
|
281
|
+
const signature = await currentSession.ringVrfSign('caller.dot', keyHandle, new Uint8Array([0x48, 0x69]));
|
|
247
282
|
```
|
|
248
283
|
|
|
284
|
+
Producing a proof or a signature with a **foreign** key handle is gated on the key's owning product having allowlisted
|
|
285
|
+
the caller in its manifest, and there is deliberately no user-prompt fallback: `message` is opaque, so consenting to it
|
|
286
|
+
is not meaningful consent, and only the owner is positioned to evaluate the risk. Reading an alias authorizes nothing
|
|
287
|
+
and stays on the ordinary grant-or-prompt path.
|
|
288
|
+
|
|
249
289
|
## Product subtree public keys
|
|
250
290
|
|
|
251
291
|
Product accounts live at `//product//{productId}/{index}` (RFC 0022). The product junction is **hard**, so the user's
|
|
@@ -261,9 +301,16 @@ subtreeKey.match(
|
|
|
261
301
|
);
|
|
262
302
|
```
|
|
263
303
|
|
|
264
|
-
The request is consent-free — the response carries no secret material. Only `AutoSigning` does: its payload is
|
|
265
|
-
product-subtree secret key
|
|
266
|
-
product's subtree
|
|
304
|
+
The request is consent-free — the response carries no secret material. Only `AutoSigning` does: its payload is the
|
|
305
|
+
product-subtree secret key (`productRootPrivateKey`, 64-byte expanded sr25519 secret), which exposes exactly that
|
|
306
|
+
product's subtree, plus `ringVrfDomainEntropy` (RFC-0024) — the entropy of the `//{productId}` node of the disjoint ring
|
|
307
|
+
VRF tree, which lets the host derive the member secret of a **registered** key locally. The former
|
|
308
|
+
`productDerivationSecret` is gone.
|
|
309
|
+
|
|
310
|
+
Bundling the entropy widens the grant: "sign transactions without prompting me" and "produce personhood proofs offline"
|
|
311
|
+
become one decision. And because derivation from the entropy is unconditional arithmetic, the registry is what
|
|
312
|
+
distinguishes a meaningful index from a meaningless one — a host MUST NOT derive a member secret for a
|
|
313
|
+
`(product, index)` pair absent from its registry.
|
|
267
314
|
|
|
268
315
|
## Identity lookups
|
|
269
316
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ export type { Credibility, Identity, IdentityAdapter, IdentityRepository } from
|
|
|
13
13
|
export { createIdentityRepository } from './identity/impl.js';
|
|
14
14
|
export { createIdentityRpcAdapter } from './identity/rpcAdapter.js';
|
|
15
15
|
export type { SignRawLegacyRequest, SignRawLegacyResponse, SigningPayloadRequest, SigningPayloadResponse, SigningRawRequest, SigningRequest, } from './sso/sessionManager/scale/signing.js';
|
|
16
|
-
export type { RingVrfAliasRequest, RingVrfAliasResponse, RingVrfProofRequest, RingVrfProofResponse, } from './sso/sessionManager/scale/ringVrf.js';
|
|
16
|
+
export type { RingVrfAliasRequest, RingVrfAliasResponse, RingVrfProofRequest, RingVrfProofResponse, RingVrfSignRequest, RingVrfSignResponse, } from './sso/sessionManager/scale/ringVrf.js';
|
|
17
|
+
export type { ListRingVrfKeysRequest, ListRingVrfKeysResponse, RegisterRingVrfKeyRequest, RegisterRingVrfKeyResponse, } from './sso/sessionManager/scale/ringVrfKeys.js';
|
|
17
18
|
export type { SignVrfErr, SignVrfRequest, SignVrfResponse } from './sso/sessionManager/scale/signVrf.js';
|
|
18
19
|
export type { CreateTransactionLegacyRequest, CreateTransactionRequest, CreateTransactionResponse, } from './sso/sessionManager/scale/createTransaction.js';
|
|
19
20
|
export type { ProductSubtreeRequest, ProductSubtreeResponse } from './sso/sessionManager/scale/productSubtree.js';
|
package/dist/sso/auth/impl.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/sso/auth/impl.js
CHANGED
|
@@ -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):
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
.
|
|
80
|
-
.
|
|
81
|
-
|
|
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())) {
|
|
@@ -67,6 +67,13 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
67
67
|
tag: "RingVrfAliasRequest";
|
|
68
68
|
value: {
|
|
69
69
|
callingProductId: string;
|
|
70
|
+
keyHandle: [string, {
|
|
71
|
+
tag: "Index";
|
|
72
|
+
value: number;
|
|
73
|
+
} | {
|
|
74
|
+
tag: "Raw";
|
|
75
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
76
|
+
}];
|
|
70
77
|
context: [string, {
|
|
71
78
|
tag: "Index";
|
|
72
79
|
value: number;
|
|
@@ -94,7 +101,7 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
94
101
|
alias: Uint8Array<ArrayBufferLike>;
|
|
95
102
|
}, import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
96
103
|
reason: string;
|
|
97
|
-
}, "GetAliasErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::RingNotFound"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::NotMember">>;
|
|
104
|
+
}, "GetAliasErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::RingNotFound"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::NotMember"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::KeyNotRegistered"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::KeyNotInRing">>;
|
|
98
105
|
};
|
|
99
106
|
} | {
|
|
100
107
|
tag: "ResourceAllocationRequest";
|
|
@@ -142,6 +149,7 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
142
149
|
tag: "AutoSigning";
|
|
143
150
|
value: {
|
|
144
151
|
productRootPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
152
|
+
ringVrfDomainEntropy: Uint8Array<ArrayBufferLike>;
|
|
145
153
|
};
|
|
146
154
|
} | {
|
|
147
155
|
tag: "BulletInAllowance";
|
|
@@ -224,6 +232,13 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
224
232
|
tag: "RingVrfProofRequest";
|
|
225
233
|
value: {
|
|
226
234
|
callingProductId: string;
|
|
235
|
+
keyHandle: [string, {
|
|
236
|
+
tag: "Index";
|
|
237
|
+
value: number;
|
|
238
|
+
} | {
|
|
239
|
+
tag: "Raw";
|
|
240
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
241
|
+
}];
|
|
227
242
|
context: [string, {
|
|
228
243
|
tag: "Index";
|
|
229
244
|
value: number;
|
|
@@ -257,7 +272,7 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
257
272
|
ringRevision: number;
|
|
258
273
|
}, import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
259
274
|
reason: string;
|
|
260
|
-
}, "CreateProofErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::RingNotFound"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::NotMember">>;
|
|
275
|
+
}, "CreateProofErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::RingNotFound"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::NotMember"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::KeyNotRegistered"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::KeyNotInRing"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::NotAllowlisted">>;
|
|
261
276
|
};
|
|
262
277
|
} | {
|
|
263
278
|
tag: "SignVrfRequest";
|
|
@@ -300,6 +315,91 @@ export declare const RemoteMessageCodec: import("scale-ts").Codec<{
|
|
|
300
315
|
productPublicKey: Uint8Array<ArrayBufferLike>;
|
|
301
316
|
}, string>;
|
|
302
317
|
};
|
|
318
|
+
} | {
|
|
319
|
+
tag: "RegisterRingVrfKeyRequest";
|
|
320
|
+
value: {
|
|
321
|
+
callingProductId: string;
|
|
322
|
+
index: {
|
|
323
|
+
tag: "Index";
|
|
324
|
+
value: number;
|
|
325
|
+
} | {
|
|
326
|
+
tag: "Raw";
|
|
327
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
328
|
+
};
|
|
329
|
+
ring: {
|
|
330
|
+
chainId: `0x${string}`;
|
|
331
|
+
junctions: ({
|
|
332
|
+
tag: "PalletInstance";
|
|
333
|
+
value: number;
|
|
334
|
+
} | {
|
|
335
|
+
tag: "CollectionId";
|
|
336
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
337
|
+
})[];
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
} | {
|
|
341
|
+
tag: "RegisterRingVrfKeyResponse";
|
|
342
|
+
value: {
|
|
343
|
+
respondingTo: string;
|
|
344
|
+
payload: import("scale-ts").ResultPayload<Uint8Array<ArrayBufferLike>, import("@novasamatech/scale").CodecError<undefined, "RegisterRingVrfKeyErr::NotConnected"> | import("@novasamatech/scale").CodecError<undefined, "RegisterRingVrfKeyErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
345
|
+
reason: string;
|
|
346
|
+
}, "RegisterRingVrfKeyErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "RegisterRingVrfKeyErr::RingNotFound">>;
|
|
347
|
+
};
|
|
348
|
+
} | {
|
|
349
|
+
tag: "ListRingVrfKeysRequest";
|
|
350
|
+
value: {
|
|
351
|
+
callingProductId: string;
|
|
352
|
+
owner: string;
|
|
353
|
+
disclosure: "Anonymized" | "PublicKey";
|
|
354
|
+
};
|
|
355
|
+
} | {
|
|
356
|
+
tag: "ListRingVrfKeysResponse";
|
|
357
|
+
value: {
|
|
358
|
+
respondingTo: string;
|
|
359
|
+
payload: import("scale-ts").ResultPayload<{
|
|
360
|
+
handle: [string, {
|
|
361
|
+
tag: "Index";
|
|
362
|
+
value: number;
|
|
363
|
+
} | {
|
|
364
|
+
tag: "Raw";
|
|
365
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
366
|
+
}];
|
|
367
|
+
rings: {
|
|
368
|
+
chainId: `0x${string}`;
|
|
369
|
+
junctions: ({
|
|
370
|
+
tag: "PalletInstance";
|
|
371
|
+
value: number;
|
|
372
|
+
} | {
|
|
373
|
+
tag: "CollectionId";
|
|
374
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
375
|
+
})[];
|
|
376
|
+
}[];
|
|
377
|
+
publicKey: Uint8Array<ArrayBufferLike> | undefined;
|
|
378
|
+
}[], import("@novasamatech/scale").CodecError<undefined, "ListRingVrfKeysErr::NotConnected"> | import("@novasamatech/scale").CodecError<undefined, "ListRingVrfKeysErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
379
|
+
reason: string;
|
|
380
|
+
}, "ListRingVrfKeysErr::Unknown">>;
|
|
381
|
+
};
|
|
382
|
+
} | {
|
|
383
|
+
tag: "RingVrfSignRequest";
|
|
384
|
+
value: {
|
|
385
|
+
callingProductId: string;
|
|
386
|
+
keyHandle: [string, {
|
|
387
|
+
tag: "Index";
|
|
388
|
+
value: number;
|
|
389
|
+
} | {
|
|
390
|
+
tag: "Raw";
|
|
391
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
392
|
+
}];
|
|
393
|
+
message: Uint8Array<ArrayBufferLike>;
|
|
394
|
+
};
|
|
395
|
+
} | {
|
|
396
|
+
tag: "RingVrfSignResponse";
|
|
397
|
+
value: {
|
|
398
|
+
respondingTo: string;
|
|
399
|
+
payload: import("scale-ts").ResultPayload<Uint8Array<ArrayBufferLike>, import("@novasamatech/scale").CodecError<undefined, "RingVrfSignErr::NotConnected"> | import("@novasamatech/scale").CodecError<undefined, "RingVrfSignErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
400
|
+
reason: string;
|
|
401
|
+
}, "RingVrfSignErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "RingVrfSignErr::KeyNotRegistered"> | import("@novasamatech/scale").CodecError<undefined, "RingVrfSignErr::NotAllowlisted">>;
|
|
402
|
+
};
|
|
303
403
|
};
|
|
304
404
|
};
|
|
305
405
|
}>;
|
|
@@ -3,7 +3,8 @@ import { Struct, _void, str } from 'scale-ts';
|
|
|
3
3
|
import { CreateTransactionLegacyRequestCodec, CreateTransactionRequestCodec, CreateTransactionResponseCodec, } from './createTransaction.js';
|
|
4
4
|
import { ProductSubtreeRequestCodec, ProductSubtreeResponseCodec } from './productSubtree.js';
|
|
5
5
|
import { ResourceAllocationRequestCodec, ResourceAllocationResponseCodec } from './resourceAllocation.js';
|
|
6
|
-
import { RingVrfAliasRequestCodec, RingVrfAliasResponseCodec, RingVrfProofRequestCodec, RingVrfProofResponseCodec, } from './ringVrf.js';
|
|
6
|
+
import { RingVrfAliasRequestCodec, RingVrfAliasResponseCodec, RingVrfProofRequestCodec, RingVrfProofResponseCodec, RingVrfSignRequestCodec, RingVrfSignResponseCodec, } from './ringVrf.js';
|
|
7
|
+
import { ListRingVrfKeysRequestCodec, ListRingVrfKeysResponseCodec, RegisterRingVrfKeyRequestCodec, RegisterRingVrfKeyResponseCodec, } from './ringVrfKeys.js';
|
|
7
8
|
import { SignVrfRequestCodec, SignVrfResponseCodec } from './signVrf.js';
|
|
8
9
|
import { SignRawLegacyRequestCodec, SignRawLegacyResponseCodec, SigningRequestCodec, SigningResponseCodec, } from './signing.js';
|
|
9
10
|
export const RemoteMessageCodec = Struct({
|
|
@@ -31,6 +32,15 @@ export const RemoteMessageCodec = Struct({
|
|
|
31
32
|
// RFC-0022 additions — appended so existing variant indexes stay stable.
|
|
32
33
|
ProductSubtreeRequest: ProductSubtreeRequestCodec,
|
|
33
34
|
ProductSubtreeResponse: ProductSubtreeResponseCodec,
|
|
35
|
+
// RFC-0024 additions — likewise appended. `RingVrfAliasRequest` and
|
|
36
|
+
// `RingVrfProofRequest` above gained a `keyHandle` field in place rather
|
|
37
|
+
// than being re-added here, since a handle is now mandatory on both.
|
|
38
|
+
RegisterRingVrfKeyRequest: RegisterRingVrfKeyRequestCodec,
|
|
39
|
+
RegisterRingVrfKeyResponse: RegisterRingVrfKeyResponseCodec,
|
|
40
|
+
ListRingVrfKeysRequest: ListRingVrfKeysRequestCodec,
|
|
41
|
+
ListRingVrfKeysResponse: ListRingVrfKeysResponseCodec,
|
|
42
|
+
RingVrfSignRequest: RingVrfSignRequestCodec,
|
|
43
|
+
RingVrfSignResponse: RingVrfSignResponseCodec,
|
|
34
44
|
}),
|
|
35
45
|
}),
|
|
36
46
|
});
|
|
@@ -32,6 +32,7 @@ export declare const ApAllocatedResourceCodec: import("scale-ts").Codec<{
|
|
|
32
32
|
tag: "AutoSigning";
|
|
33
33
|
value: {
|
|
34
34
|
productRootPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
35
|
+
ringVrfDomainEntropy: Uint8Array<ArrayBufferLike>;
|
|
35
36
|
};
|
|
36
37
|
} | {
|
|
37
38
|
tag: "BulletInAllowance";
|
|
@@ -57,6 +58,7 @@ export declare const ApAllocationOutcomeCodec: import("scale-ts").Codec<{
|
|
|
57
58
|
tag: "AutoSigning";
|
|
58
59
|
value: {
|
|
59
60
|
productRootPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
61
|
+
ringVrfDomainEntropy: Uint8Array<ArrayBufferLike>;
|
|
60
62
|
};
|
|
61
63
|
} | {
|
|
62
64
|
tag: "BulletInAllowance";
|
|
@@ -113,6 +115,7 @@ export declare const ResourceAllocationResponseCodec: import("scale-ts").Codec<{
|
|
|
113
115
|
tag: "AutoSigning";
|
|
114
116
|
value: {
|
|
115
117
|
productRootPrivateKey: Uint8Array<ArrayBufferLike>;
|
|
118
|
+
ringVrfDomainEntropy: Uint8Array<ArrayBufferLike>;
|
|
116
119
|
};
|
|
117
120
|
} | {
|
|
118
121
|
tag: "BulletInAllowance";
|
|
@@ -23,6 +23,17 @@ export const ApAllocatedResourceCodec = Enum({
|
|
|
23
23
|
// `Sr25519PrivateKey ++ Sr25519Nonce` (64 bytes): the full expanded secret
|
|
24
24
|
// needed to sign and soft-derive `/{index}` below the product root.
|
|
25
25
|
productRootPrivateKey: Bytes(),
|
|
26
|
+
// RFC-0024: entropy of the `//{productId}` node of the ring VRF tree, which
|
|
27
|
+
// is disjoint from the sr25519 product-account tree above. It lets the Host
|
|
28
|
+
// derive the member secret of a *registered* key locally — the second
|
|
29
|
+
// motivation being that a headless Account Holder execution context may not
|
|
30
|
+
// fit a ring VRF proof inside its ~30 s / ~24 MB budget.
|
|
31
|
+
//
|
|
32
|
+
// Derivation from this entropy is unconditional arithmetic: nothing about
|
|
33
|
+
// holding it distinguishes a meaningful index from a meaningless one, so the
|
|
34
|
+
// registry supplies that distinction. A Host MUST NOT derive a member secret
|
|
35
|
+
// for a `(product, index)` pair absent from its registry.
|
|
36
|
+
ringVrfDomainEntropy: Bytes(),
|
|
26
37
|
}),
|
|
27
38
|
});
|
|
28
39
|
export const ApAllocationOutcomeCodec = Enum({
|
|
@@ -2,6 +2,13 @@ import type { CodecType } from 'scale-ts';
|
|
|
2
2
|
export type RingVrfAliasRequest = CodecType<typeof RingVrfAliasRequestCodec>;
|
|
3
3
|
export declare const RingVrfAliasRequestCodec: import("scale-ts").Codec<{
|
|
4
4
|
callingProductId: string;
|
|
5
|
+
keyHandle: [string, {
|
|
6
|
+
tag: "Index";
|
|
7
|
+
value: number;
|
|
8
|
+
} | {
|
|
9
|
+
tag: "Raw";
|
|
10
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
11
|
+
}];
|
|
5
12
|
context: [string, {
|
|
6
13
|
tag: "Index";
|
|
7
14
|
value: number;
|
|
@@ -28,11 +35,18 @@ export declare const RingVrfAliasResponseCodec: import("scale-ts").Codec<{
|
|
|
28
35
|
alias: Uint8Array<ArrayBufferLike>;
|
|
29
36
|
}, import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
30
37
|
reason: string;
|
|
31
|
-
}, "GetAliasErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::RingNotFound"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::NotMember">>;
|
|
38
|
+
}, "GetAliasErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::RingNotFound"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::NotMember"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::KeyNotRegistered"> | import("@novasamatech/scale").CodecError<undefined, "GetAliasErr::KeyNotInRing">>;
|
|
32
39
|
}>;
|
|
33
40
|
export type RingVrfProofRequest = CodecType<typeof RingVrfProofRequestCodec>;
|
|
34
41
|
export declare const RingVrfProofRequestCodec: import("scale-ts").Codec<{
|
|
35
42
|
callingProductId: string;
|
|
43
|
+
keyHandle: [string, {
|
|
44
|
+
tag: "Index";
|
|
45
|
+
value: number;
|
|
46
|
+
} | {
|
|
47
|
+
tag: "Raw";
|
|
48
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
49
|
+
}];
|
|
36
50
|
context: [string, {
|
|
37
51
|
tag: "Index";
|
|
38
52
|
value: number;
|
|
@@ -65,5 +79,32 @@ export declare const RingVrfProofResponseCodec: import("scale-ts").Codec<{
|
|
|
65
79
|
ringRevision: number;
|
|
66
80
|
}, import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
67
81
|
reason: string;
|
|
68
|
-
}, "CreateProofErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::RingNotFound"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::NotMember">>;
|
|
82
|
+
}, "CreateProofErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::RingNotFound"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::NotMember"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::KeyNotRegistered"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::KeyNotInRing"> | import("@novasamatech/scale").CodecError<undefined, "CreateProofErr::NotAllowlisted">>;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Host → Account Holder request for a plain signature by the ring VRF member key
|
|
86
|
+
* itself (RFC-0024 `ring_vrf_sign`), rather than an anonymous ring proof.
|
|
87
|
+
*
|
|
88
|
+
* Carries no context and no ring: the signature derives no alias and proves no
|
|
89
|
+
* membership, so there is nothing for either to scope. It is verified against
|
|
90
|
+
* the member public key and is therefore linkable to every other use of the key.
|
|
91
|
+
*/
|
|
92
|
+
export type RingVrfSignRequest = CodecType<typeof RingVrfSignRequestCodec>;
|
|
93
|
+
export declare const RingVrfSignRequestCodec: import("scale-ts").Codec<{
|
|
94
|
+
callingProductId: string;
|
|
95
|
+
keyHandle: [string, {
|
|
96
|
+
tag: "Index";
|
|
97
|
+
value: number;
|
|
98
|
+
} | {
|
|
99
|
+
tag: "Raw";
|
|
100
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
101
|
+
}];
|
|
102
|
+
message: Uint8Array<ArrayBufferLike>;
|
|
103
|
+
}>;
|
|
104
|
+
export type RingVrfSignResponse = CodecType<typeof RingVrfSignResponseCodec>;
|
|
105
|
+
export declare const RingVrfSignResponseCodec: import("scale-ts").Codec<{
|
|
106
|
+
respondingTo: string;
|
|
107
|
+
payload: import("scale-ts").ResultPayload<Uint8Array<ArrayBufferLike>, import("@novasamatech/scale").CodecError<undefined, "RingVrfSignErr::NotConnected"> | import("@novasamatech/scale").CodecError<undefined, "RingVrfSignErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
108
|
+
reason: string;
|
|
109
|
+
}, "RingVrfSignErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "RingVrfSignErr::KeyNotRegistered"> | import("@novasamatech/scale").CodecError<undefined, "RingVrfSignErr::NotAllowlisted">>;
|
|
69
110
|
}>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { ContextualAlias, CreateProofErr, DotNsIdentifier, GetAliasErr, ProductProofContext, RingLocation, RingVrfProof, } from '@novasamatech/host-api';
|
|
1
|
+
import { ContextualAlias, CreateProofErr, DotNsIdentifier, GetAliasErr, ProductProofContext, RingLocation, RingVrfKeyHandle, RingVrfProof, RingVrfSignErr, } from '@novasamatech/host-api';
|
|
2
2
|
import { Bytes } from '@novasamatech/scale';
|
|
3
3
|
import { Result, Struct, str } from 'scale-ts';
|
|
4
4
|
export const RingVrfAliasRequestCodec = Struct({
|
|
5
5
|
callingProductId: DotNsIdentifier,
|
|
6
|
+
keyHandle: RingVrfKeyHandle,
|
|
6
7
|
context: ProductProofContext,
|
|
7
8
|
ring: RingLocation,
|
|
8
9
|
});
|
|
@@ -12,6 +13,7 @@ export const RingVrfAliasResponseCodec = Struct({
|
|
|
12
13
|
});
|
|
13
14
|
export const RingVrfProofRequestCodec = Struct({
|
|
14
15
|
callingProductId: DotNsIdentifier,
|
|
16
|
+
keyHandle: RingVrfKeyHandle,
|
|
15
17
|
context: ProductProofContext,
|
|
16
18
|
ring: RingLocation,
|
|
17
19
|
message: Bytes(),
|
|
@@ -20,3 +22,12 @@ export const RingVrfProofResponseCodec = Struct({
|
|
|
20
22
|
respondingTo: str,
|
|
21
23
|
payload: Result(RingVrfProof, CreateProofErr),
|
|
22
24
|
});
|
|
25
|
+
export const RingVrfSignRequestCodec = Struct({
|
|
26
|
+
callingProductId: DotNsIdentifier,
|
|
27
|
+
keyHandle: RingVrfKeyHandle,
|
|
28
|
+
message: Bytes(),
|
|
29
|
+
});
|
|
30
|
+
export const RingVrfSignResponseCodec = Struct({
|
|
31
|
+
respondingTo: str,
|
|
32
|
+
payload: Result(Bytes(), RingVrfSignErr),
|
|
33
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { CodecType } from 'scale-ts';
|
|
2
|
+
/**
|
|
3
|
+
* Host → Account Holder registration of a key the calling product owns.
|
|
4
|
+
*
|
|
5
|
+
* Ownership is `callingProductId` and is never chosen by the caller, so this is
|
|
6
|
+
* consent-free. Registration is idempotent: re-registering an `index` for an
|
|
7
|
+
* additional `ring` extends the existing entry rather than creating a second
|
|
8
|
+
* one, so re-notifying the phone about an entry it already has costs nothing.
|
|
9
|
+
*
|
|
10
|
+
* A Host holding the product's ring VRF domain entropy (see `AutoSigning` in
|
|
11
|
+
* `resourceAllocation.ts`) answers immediately and mirrors this fire-and-forget;
|
|
12
|
+
* without the entropy it issues the request and waits.
|
|
13
|
+
*
|
|
14
|
+
* > A Host MUST NOT derive a member secret for a `(product, index)` pair absent
|
|
15
|
+
* > from its registry. Domain entropy makes derivation unconditional — only
|
|
16
|
+
* > registration, which always reaches the phone, brings a key into existence.
|
|
17
|
+
*/
|
|
18
|
+
export type RegisterRingVrfKeyRequest = CodecType<typeof RegisterRingVrfKeyRequestCodec>;
|
|
19
|
+
export declare const RegisterRingVrfKeyRequestCodec: import("scale-ts").Codec<{
|
|
20
|
+
callingProductId: string;
|
|
21
|
+
index: {
|
|
22
|
+
tag: "Index";
|
|
23
|
+
value: number;
|
|
24
|
+
} | {
|
|
25
|
+
tag: "Raw";
|
|
26
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
27
|
+
};
|
|
28
|
+
ring: {
|
|
29
|
+
chainId: `0x${string}`;
|
|
30
|
+
junctions: ({
|
|
31
|
+
tag: "PalletInstance";
|
|
32
|
+
value: number;
|
|
33
|
+
} | {
|
|
34
|
+
tag: "CollectionId";
|
|
35
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
36
|
+
})[];
|
|
37
|
+
};
|
|
38
|
+
}>;
|
|
39
|
+
export type RegisterRingVrfKeyResponse = CodecType<typeof RegisterRingVrfKeyResponseCodec>;
|
|
40
|
+
export declare const RegisterRingVrfKeyResponseCodec: import("scale-ts").Codec<{
|
|
41
|
+
respondingTo: string;
|
|
42
|
+
payload: import("scale-ts").ResultPayload<Uint8Array<ArrayBufferLike>, import("@novasamatech/scale").CodecError<undefined, "RegisterRingVrfKeyErr::NotConnected"> | import("@novasamatech/scale").CodecError<undefined, "RegisterRingVrfKeyErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
43
|
+
reason: string;
|
|
44
|
+
}, "RegisterRingVrfKeyErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "RegisterRingVrfKeyErr::RingNotFound">>;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Host → Account Holder listing of the registry entries owned by `owner`.
|
|
48
|
+
*
|
|
49
|
+
* `owner` may be `callingProductId` — permissionless — or another product, in
|
|
50
|
+
* which case the caller needs a grant. `PublicKey` disclosure additionally
|
|
51
|
+
* returns the member public key, which is linkable across every ring it appears
|
|
52
|
+
* in and so is permissioned cross-product even though the handle is not.
|
|
53
|
+
*/
|
|
54
|
+
export type ListRingVrfKeysRequest = CodecType<typeof ListRingVrfKeysRequestCodec>;
|
|
55
|
+
export declare const ListRingVrfKeysRequestCodec: import("scale-ts").Codec<{
|
|
56
|
+
callingProductId: string;
|
|
57
|
+
owner: string;
|
|
58
|
+
disclosure: "Anonymized" | "PublicKey";
|
|
59
|
+
}>;
|
|
60
|
+
export type ListRingVrfKeysResponse = CodecType<typeof ListRingVrfKeysResponseCodec>;
|
|
61
|
+
export declare const ListRingVrfKeysResponseCodec: import("scale-ts").Codec<{
|
|
62
|
+
respondingTo: string;
|
|
63
|
+
payload: import("scale-ts").ResultPayload<{
|
|
64
|
+
handle: [string, {
|
|
65
|
+
tag: "Index";
|
|
66
|
+
value: number;
|
|
67
|
+
} | {
|
|
68
|
+
tag: "Raw";
|
|
69
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
70
|
+
}];
|
|
71
|
+
rings: {
|
|
72
|
+
chainId: `0x${string}`;
|
|
73
|
+
junctions: ({
|
|
74
|
+
tag: "PalletInstance";
|
|
75
|
+
value: number;
|
|
76
|
+
} | {
|
|
77
|
+
tag: "CollectionId";
|
|
78
|
+
value: Uint8Array<ArrayBufferLike>;
|
|
79
|
+
})[];
|
|
80
|
+
}[];
|
|
81
|
+
publicKey: Uint8Array<ArrayBufferLike> | undefined;
|
|
82
|
+
}[], import("@novasamatech/scale").CodecError<undefined, "ListRingVrfKeysErr::NotConnected"> | import("@novasamatech/scale").CodecError<undefined, "ListRingVrfKeysErr::Rejected"> | import("@novasamatech/scale").CodecError<{
|
|
83
|
+
reason: string;
|
|
84
|
+
}, "ListRingVrfKeysErr::Unknown">>;
|
|
85
|
+
}>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DerivationIndex, DotNsIdentifier, ListRingVrfKeysErr, RegisterRingVrfKeyErr, RegisteredRingVrfKey, RingLocation, RingVrfKeyDisclosure, RingVrfPublicKey, } from '@novasamatech/host-api';
|
|
2
|
+
import { Result, Struct, Vector, str } from 'scale-ts';
|
|
3
|
+
export const RegisterRingVrfKeyRequestCodec = Struct({
|
|
4
|
+
callingProductId: DotNsIdentifier,
|
|
5
|
+
index: DerivationIndex,
|
|
6
|
+
ring: RingLocation,
|
|
7
|
+
});
|
|
8
|
+
export const RegisterRingVrfKeyResponseCodec = Struct({
|
|
9
|
+
// referencing to RemoteMessage.messageId
|
|
10
|
+
respondingTo: str,
|
|
11
|
+
payload: Result(RingVrfPublicKey, RegisterRingVrfKeyErr),
|
|
12
|
+
});
|
|
13
|
+
export const ListRingVrfKeysRequestCodec = Struct({
|
|
14
|
+
callingProductId: DotNsIdentifier,
|
|
15
|
+
owner: DotNsIdentifier,
|
|
16
|
+
disclosure: RingVrfKeyDisclosure,
|
|
17
|
+
});
|
|
18
|
+
export const ListRingVrfKeysResponseCodec = Struct({
|
|
19
|
+
// referencing to RemoteMessage.messageId
|
|
20
|
+
respondingTo: str,
|
|
21
|
+
payload: Result(Vector(RegisteredRingVrfKey), ListRingVrfKeysErr),
|
|
22
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContextualAlias, ProductProofContext, RingLocation, RingVrfProof, VrfSignature } from '@novasamatech/host-api';
|
|
1
|
+
import { ContextualAlias, DerivationIndex, ProductProofContext, RegisteredRingVrfKey, RingLocation, RingVrfKeyDisclosure, RingVrfKeyHandle, RingVrfProof, VrfSignature } from '@novasamatech/host-api';
|
|
2
2
|
import type { Encryption, StatementProver, StatementStoreAdapter } from '@novasamatech/statement-store';
|
|
3
3
|
import type { StorageAdapter } from '@novasamatech/storage-adapter';
|
|
4
4
|
import { ResultAsync } from 'neverthrow';
|
|
@@ -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>;
|
|
@@ -24,8 +25,27 @@ export type UserSession = StoredUserSession & {
|
|
|
24
25
|
signRawLegacy(payload: SignRawLegacyRequest): ResultAsync<Uint8Array, Error>;
|
|
25
26
|
createTransaction(payload: CreateTransactionRequest): ResultAsync<Uint8Array, Error>;
|
|
26
27
|
createTransactionLegacy(payload: CreateTransactionLegacyRequest): ResultAsync<Uint8Array, Error>;
|
|
27
|
-
getRingVrfAlias(callingProductId: string, context: CodecType<typeof ProductProofContext>, ring: CodecType<typeof RingLocation>): ResultAsync<CodecType<typeof ContextualAlias>, Error>;
|
|
28
|
-
createRingVrfProof(callingProductId: string, context: CodecType<typeof ProductProofContext>, ring: CodecType<typeof RingLocation>, message: Uint8Array): ResultAsync<CodecType<typeof RingVrfProof>, Error>;
|
|
28
|
+
getRingVrfAlias(callingProductId: string, keyHandle: CodecType<typeof RingVrfKeyHandle>, context: CodecType<typeof ProductProofContext>, ring: CodecType<typeof RingLocation>): ResultAsync<CodecType<typeof ContextualAlias>, Error>;
|
|
29
|
+
createRingVrfProof(callingProductId: string, keyHandle: CodecType<typeof RingVrfKeyHandle>, context: CodecType<typeof ProductProofContext>, ring: CodecType<typeof RingLocation>, message: Uint8Array): ResultAsync<CodecType<typeof RingVrfProof>, Error>;
|
|
30
|
+
/**
|
|
31
|
+
* Signs `message` with the ring VRF member key itself (RFC-0024).
|
|
32
|
+
*
|
|
33
|
+
* Unlike {@link createRingVrfProof} this is not anonymous: the result is
|
|
34
|
+
* verified against the member public key and is linkable to every other use
|
|
35
|
+
* of that key.
|
|
36
|
+
*/
|
|
37
|
+
ringVrfSign(callingProductId: string, keyHandle: CodecType<typeof RingVrfKeyHandle>, message: Uint8Array): ResultAsync<Uint8Array, Error>;
|
|
38
|
+
/**
|
|
39
|
+
* Registers a ring VRF key owned by `callingProductId` against `ring`,
|
|
40
|
+
* returning the member public key (RFC-0024).
|
|
41
|
+
*
|
|
42
|
+
* The Account Holder is the authoritative registry, so registration always
|
|
43
|
+
* reaches it. Idempotent — re-registering an index for an additional ring
|
|
44
|
+
* extends the existing entry.
|
|
45
|
+
*/
|
|
46
|
+
registerRingVrfKey(callingProductId: string, index: CodecType<typeof DerivationIndex>, ring: CodecType<typeof RingLocation>): ResultAsync<Uint8Array, Error>;
|
|
47
|
+
/** Lists the ring VRF registry entries owned by `owner` (RFC-0024). */
|
|
48
|
+
listRingVrfKeys(callingProductId: string, owner: string, disclosure: CodecType<typeof RingVrfKeyDisclosure>): ResultAsync<CodecType<typeof RegisteredRingVrfKey>[], Error>;
|
|
29
49
|
/**
|
|
30
50
|
* Fetches the sr25519 public key of `//product//{productId}` (RFC-0022).
|
|
31
51
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContextualAlias, ProductProofContext, RingLocation, RingVrfProof, VrfSignature } from '@novasamatech/host-api';
|
|
1
|
+
import { ContextualAlias, DerivationIndex, ProductProofContext, RegisteredRingVrfKey, RingLocation, RingVrfKeyDisclosure, RingVrfKeyHandle, RingVrfProof, VrfSignature, } from '@novasamatech/host-api';
|
|
2
2
|
import { enumValue } from '@novasamatech/scale';
|
|
3
3
|
import { createSession } from '@novasamatech/statement-store';
|
|
4
4
|
import { fieldListView } from '@novasamatech/storage-adapter';
|
|
@@ -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:
|
|
111
|
+
key: processedMessagesKey(userSession.id),
|
|
110
112
|
from: JSON.parse,
|
|
111
113
|
to: JSON.stringify,
|
|
112
114
|
});
|
|
@@ -244,11 +246,12 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
244
246
|
return withHostActionTrace(withQueueTimeout(inner, 'createTransactionLegacy'), messageId, userSession.id);
|
|
245
247
|
});
|
|
246
248
|
},
|
|
247
|
-
getRingVrfAlias(callingProductId, context, ring) {
|
|
249
|
+
getRingVrfAlias(callingProductId, keyHandle, context, ring) {
|
|
248
250
|
return enqueue(() => {
|
|
249
251
|
const messageId = nanoid();
|
|
250
252
|
const data = enumValue('v1', enumValue('RingVrfAliasRequest', {
|
|
251
253
|
callingProductId,
|
|
254
|
+
keyHandle,
|
|
252
255
|
context,
|
|
253
256
|
ring,
|
|
254
257
|
}));
|
|
@@ -265,11 +268,12 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
265
268
|
return withHostActionTrace(awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value)), messageId, userSession.id);
|
|
266
269
|
});
|
|
267
270
|
},
|
|
268
|
-
createRingVrfProof(callingProductId, context, ring, message) {
|
|
271
|
+
createRingVrfProof(callingProductId, keyHandle, context, ring, message) {
|
|
269
272
|
return enqueue(() => {
|
|
270
273
|
const messageId = nanoid();
|
|
271
274
|
const data = enumValue('v1', enumValue('RingVrfProofRequest', {
|
|
272
275
|
callingProductId,
|
|
276
|
+
keyHandle,
|
|
273
277
|
context,
|
|
274
278
|
ring,
|
|
275
279
|
message,
|
|
@@ -287,6 +291,60 @@ export function createUserSession({ userSession, statementStore, encryption, sto
|
|
|
287
291
|
return withHostActionTrace(awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value)), messageId, userSession.id);
|
|
288
292
|
});
|
|
289
293
|
},
|
|
294
|
+
ringVrfSign(callingProductId, keyHandle, message) {
|
|
295
|
+
return enqueue(() => {
|
|
296
|
+
const messageId = nanoid();
|
|
297
|
+
const data = enumValue('v1', enumValue('RingVrfSignRequest', { callingProductId, keyHandle, message }));
|
|
298
|
+
emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
|
|
299
|
+
const responseFilter = (incoming) => {
|
|
300
|
+
if (incoming.data.tag === 'v1' &&
|
|
301
|
+
incoming.data.value.tag === 'RingVrfSignResponse' &&
|
|
302
|
+
incoming.data.value.value.respondingTo === messageId) {
|
|
303
|
+
return incoming.data.value.value.payload;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
307
|
+
const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
|
|
308
|
+
const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value));
|
|
309
|
+
return withHostActionTrace(withQueueTimeout(inner, 'ringVrfSign'), messageId, userSession.id);
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
registerRingVrfKey(callingProductId, index, ring) {
|
|
313
|
+
return enqueue(() => {
|
|
314
|
+
const messageId = nanoid();
|
|
315
|
+
const data = enumValue('v1', enumValue('RegisterRingVrfKeyRequest', { callingProductId, index, ring }));
|
|
316
|
+
emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
|
|
317
|
+
const responseFilter = (incoming) => {
|
|
318
|
+
if (incoming.data.tag === 'v1' &&
|
|
319
|
+
incoming.data.value.tag === 'RegisterRingVrfKeyResponse' &&
|
|
320
|
+
incoming.data.value.value.respondingTo === messageId) {
|
|
321
|
+
return incoming.data.value.value.payload;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
325
|
+
const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
|
|
326
|
+
const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value));
|
|
327
|
+
return withHostActionTrace(withQueueTimeout(inner, 'registerRingVrfKey'), messageId, userSession.id);
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
listRingVrfKeys(callingProductId, owner, disclosure) {
|
|
331
|
+
return enqueue(() => {
|
|
332
|
+
const messageId = nanoid();
|
|
333
|
+
const data = enumValue('v1', enumValue('ListRingVrfKeysRequest', { callingProductId, owner, disclosure }));
|
|
334
|
+
emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
|
|
335
|
+
const responseFilter = (incoming) => {
|
|
336
|
+
if (incoming.data.tag === 'v1' &&
|
|
337
|
+
incoming.data.value.tag === 'ListRingVrfKeysResponse' &&
|
|
338
|
+
incoming.data.value.value.respondingTo === messageId) {
|
|
339
|
+
return incoming.data.value.value.payload;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const request = session.request(RemoteMessageCodec, { messageId, data });
|
|
343
|
+
const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
|
|
344
|
+
const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value));
|
|
345
|
+
return withHostActionTrace(withQueueTimeout(inner, 'listRingVrfKeys'), messageId, userSession.id);
|
|
346
|
+
});
|
|
347
|
+
},
|
|
290
348
|
getProductSubtree(productId) {
|
|
291
349
|
return enqueue(() => {
|
|
292
350
|
const messageId = nanoid();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novasamatech/host-papp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
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.
|
|
42
|
-
"@novasamatech/scale": "0.9.
|
|
43
|
-
"@novasamatech/statement-store": "0.9.
|
|
44
|
-
"@novasamatech/storage-adapter": "0.9.
|
|
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",
|