@novasamatech/host-papp 0.9.2 → 0.9.3

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 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
- A `UserSession` can ask the paired device for a privacy-preserving contextual alias, or a ring VRF proof, for a
224
- product-scoped `context` and a `ring` location. The device selects the member key for the ring; `callingProductId` names
225
- the product the host is acting for. Both take the same `(context, ring)` so the alias in the proof matches
226
- `getRingVrfAlias`.
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
- const alias = await currentSession.getRingVrfAlias('caller.dot', context, ring);
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
- const proof = await currentSession.createRingVrfProof('caller.dot', context, ring, new Uint8Array([0x48, 0x69]));
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 `RingVrfError` (RingNotFound / NotMember / Rejected / Unknown)
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 now the
265
- product-subtree secret key alone (`productRootPrivateKey`, 64-byte expanded sr25519 secret), which exposes exactly that
266
- product's subtree. The former `productDerivationSecret` is gone.
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';
@@ -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';
@@ -24,8 +24,27 @@ export type UserSession = StoredUserSession & {
24
24
  signRawLegacy(payload: SignRawLegacyRequest): ResultAsync<Uint8Array, Error>;
25
25
  createTransaction(payload: CreateTransactionRequest): ResultAsync<Uint8Array, Error>;
26
26
  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>;
27
+ getRingVrfAlias(callingProductId: string, keyHandle: CodecType<typeof RingVrfKeyHandle>, context: CodecType<typeof ProductProofContext>, ring: CodecType<typeof RingLocation>): ResultAsync<CodecType<typeof ContextualAlias>, Error>;
28
+ createRingVrfProof(callingProductId: string, keyHandle: CodecType<typeof RingVrfKeyHandle>, context: CodecType<typeof ProductProofContext>, ring: CodecType<typeof RingLocation>, message: Uint8Array): ResultAsync<CodecType<typeof RingVrfProof>, Error>;
29
+ /**
30
+ * Signs `message` with the ring VRF member key itself (RFC-0024).
31
+ *
32
+ * Unlike {@link createRingVrfProof} this is not anonymous: the result is
33
+ * verified against the member public key and is linkable to every other use
34
+ * of that key.
35
+ */
36
+ ringVrfSign(callingProductId: string, keyHandle: CodecType<typeof RingVrfKeyHandle>, message: Uint8Array): ResultAsync<Uint8Array, Error>;
37
+ /**
38
+ * Registers a ring VRF key owned by `callingProductId` against `ring`,
39
+ * returning the member public key (RFC-0024).
40
+ *
41
+ * The Account Holder is the authoritative registry, so registration always
42
+ * reaches it. Idempotent — re-registering an index for an additional ring
43
+ * extends the existing entry.
44
+ */
45
+ registerRingVrfKey(callingProductId: string, index: CodecType<typeof DerivationIndex>, ring: CodecType<typeof RingLocation>): ResultAsync<Uint8Array, Error>;
46
+ /** Lists the ring VRF registry entries owned by `owner` (RFC-0024). */
47
+ listRingVrfKeys(callingProductId: string, owner: string, disclosure: CodecType<typeof RingVrfKeyDisclosure>): ResultAsync<CodecType<typeof RegisteredRingVrfKey>[], Error>;
29
48
  /**
30
49
  * Fetches the sr25519 public key of `//product//{productId}` (RFC-0022).
31
50
  *
@@ -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';
@@ -244,11 +244,12 @@ export function createUserSession({ userSession, statementStore, encryption, sto
244
244
  return withHostActionTrace(withQueueTimeout(inner, 'createTransactionLegacy'), messageId, userSession.id);
245
245
  });
246
246
  },
247
- getRingVrfAlias(callingProductId, context, ring) {
247
+ getRingVrfAlias(callingProductId, keyHandle, context, ring) {
248
248
  return enqueue(() => {
249
249
  const messageId = nanoid();
250
250
  const data = enumValue('v1', enumValue('RingVrfAliasRequest', {
251
251
  callingProductId,
252
+ keyHandle,
252
253
  context,
253
254
  ring,
254
255
  }));
@@ -265,11 +266,12 @@ export function createUserSession({ userSession, statementStore, encryption, sto
265
266
  return withHostActionTrace(awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value)), messageId, userSession.id);
266
267
  });
267
268
  },
268
- createRingVrfProof(callingProductId, context, ring, message) {
269
+ createRingVrfProof(callingProductId, keyHandle, context, ring, message) {
269
270
  return enqueue(() => {
270
271
  const messageId = nanoid();
271
272
  const data = enumValue('v1', enumValue('RingVrfProofRequest', {
272
273
  callingProductId,
274
+ keyHandle,
273
275
  context,
274
276
  ring,
275
277
  message,
@@ -287,6 +289,60 @@ export function createUserSession({ userSession, statementStore, encryption, sto
287
289
  return withHostActionTrace(awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value)), messageId, userSession.id);
288
290
  });
289
291
  },
292
+ ringVrfSign(callingProductId, keyHandle, message) {
293
+ return enqueue(() => {
294
+ const messageId = nanoid();
295
+ const data = enumValue('v1', enumValue('RingVrfSignRequest', { callingProductId, keyHandle, message }));
296
+ emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
297
+ const responseFilter = (incoming) => {
298
+ if (incoming.data.tag === 'v1' &&
299
+ incoming.data.value.tag === 'RingVrfSignResponse' &&
300
+ incoming.data.value.value.respondingTo === messageId) {
301
+ return incoming.data.value.value.payload;
302
+ }
303
+ };
304
+ const request = session.request(RemoteMessageCodec, { messageId, data });
305
+ const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
306
+ const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value));
307
+ return withHostActionTrace(withQueueTimeout(inner, 'ringVrfSign'), messageId, userSession.id);
308
+ });
309
+ },
310
+ registerRingVrfKey(callingProductId, index, ring) {
311
+ return enqueue(() => {
312
+ const messageId = nanoid();
313
+ const data = enumValue('v1', enumValue('RegisterRingVrfKeyRequest', { callingProductId, index, ring }));
314
+ emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
315
+ const responseFilter = (incoming) => {
316
+ if (incoming.data.tag === 'v1' &&
317
+ incoming.data.value.tag === 'RegisterRingVrfKeyResponse' &&
318
+ incoming.data.value.value.respondingTo === messageId) {
319
+ return incoming.data.value.value.payload;
320
+ }
321
+ };
322
+ const request = session.request(RemoteMessageCodec, { messageId, data });
323
+ const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
324
+ const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value));
325
+ return withHostActionTrace(withQueueTimeout(inner, 'registerRingVrfKey'), messageId, userSession.id);
326
+ });
327
+ },
328
+ listRingVrfKeys(callingProductId, owner, disclosure) {
329
+ return enqueue(() => {
330
+ const messageId = nanoid();
331
+ const data = enumValue('v1', enumValue('ListRingVrfKeysRequest', { callingProductId, owner, disclosure }));
332
+ emitHostAction(messageId, actionKindFromMessageData(data), userSession.id);
333
+ const responseFilter = (incoming) => {
334
+ if (incoming.data.tag === 'v1' &&
335
+ incoming.data.value.tag === 'ListRingVrfKeysResponse' &&
336
+ incoming.data.value.value.respondingTo === messageId) {
337
+ return incoming.data.value.value.payload;
338
+ }
339
+ };
340
+ const request = session.request(RemoteMessageCodec, { messageId, data });
341
+ const reply = session.waitForRequestMessage(RemoteMessageCodec, responseFilter);
342
+ const inner = awaitReplyOrAckFailure(request, reply).andThen(result => result.success ? ok(result.value) : err(result.value));
343
+ return withHostActionTrace(withQueueTimeout(inner, 'listRingVrfKeys'), messageId, userSession.id);
344
+ });
345
+ },
290
346
  getProductSubtree(productId) {
291
347
  return enqueue(() => {
292
348
  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.2",
4
+ "version": "0.9.3",
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.2",
42
- "@novasamatech/scale": "0.9.2",
43
- "@novasamatech/statement-store": "0.9.2",
44
- "@novasamatech/storage-adapter": "0.9.2",
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",
45
45
  "@polkadot-api/utils": "^0.4.0",
46
46
  "@polkadot-labs/hdkd-helpers": "^0.0.31",
47
47
  "nanoevents": "10.0.0",