Skip to content

Commit 5062bd6

Browse files
Merge upstream develop
2 parents 0da3de8 + b2b542b commit 5062bd6

23 files changed

Lines changed: 317 additions & 173 deletions

File tree

.gitlab/ci/build_rs_core.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ rs-core-tests:
5757
rs-common-format-check:
5858
extends: .rust-check
5959
script:
60-
- cd packages/rs-core
60+
- cd packages/rs-common
6161
- cargo fmt --check
6262

6363
rs-common-clippy:

apps/polycentric/src/features/identity-pairing/hooks/usePairIdentityClaimer.ts

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ export function usePairIdentityClaimer(
1717
const pairingSessionServer = options?.pairingSessionServer;
1818
const [identityKey, setIdentityKey] = useState<string | null>(null);
1919
const [error, setError] = useState<string | null>(null);
20+
const [authorized, setAuthorized] = useState(false);
2021
const [approved, setApproved] = useState(false);
2122
const [claimInProgress, setClaimInProgress] = useState(false);
2223

24+
// Join the pairing session on the server and learn the issuer identity.
2325
useEffect(() => {
2426
if (!pairingSessionCode || identityKey) return;
2527

@@ -51,51 +53,72 @@ export function usePairIdentityClaimer(
5153
claimAndWait();
5254
}, [pairingSessionCode, pairingSessionServer, identityKey, client]);
5355

56+
// Poll the issuer's identity until the current key is authorized.
57+
// Stops once `authorized` flips to true.
5458
useEffect(() => {
55-
if (!identityKey || !pairingSessionServer) return;
59+
if (!identityKey || !pairingSessionServer || authorized) return;
5660

5761
let cancelled = false;
5862

59-
const pollApproval = async () => {
63+
const pollAuthorization = async () => {
6064
try {
6165
const state = await client.identityManager.fetchIdentityState(
6266
identityKey,
6367
pairingSessionServer,
6468
);
69+
if (cancelled) return;
6570
const currentKey = client.currentKeyPair?.publicKey;
71+
if (!currentKey) return;
6672

67-
if (!currentKey || cancelled) return;
73+
const keys = new Set<string>();
74+
state.rotationKeys.forEach((k) => keys.add(publicKeyToString(k)));
75+
state.signingKeys.forEach((k) => keys.add(publicKeyToString(k)));
6876

69-
const authorized = new Set<string>();
70-
state.rotationKeys.forEach((k) => authorized.add(publicKeyToString(k)));
71-
state.signingKeys.forEach((k) => authorized.add(publicKeyToString(k)));
72-
73-
if (authorized.has(publicKeyToString(currentKey))) {
74-
// add the pairing session server to our servers list
75-
if (!client.servers.includes(pairingSessionServer)) {
76-
client.servers.push(pairingSessionServer);
77-
}
78-
79-
// fetch and apply the identity document
80-
await client.identityManager.claim(identityKey);
81-
82-
setApproved(true);
77+
if (keys.has(publicKeyToString(currentKey))) {
78+
setAuthorized(true);
8379
}
8480
} catch {
8581
// polling failed, will retry on next interval
8682
}
8783
};
8884

89-
void pollApproval();
85+
void pollAuthorization();
9086
const interval = setInterval(() => {
91-
void pollApproval();
87+
void pollAuthorization();
9288
}, 2000);
9389

9490
return () => {
9591
cancelled = true;
9692
clearInterval(interval);
9793
};
98-
}, [identityKey, pairingSessionServer, client]);
94+
}, [identityKey, pairingSessionServer, client, authorized]);
95+
96+
// Claim exactly once when authorization is observed.
97+
useEffect(() => {
98+
if (!authorized || !identityKey || !pairingSessionServer) return;
99+
100+
let cancelled = false;
101+
102+
void (async () => {
103+
try {
104+
if (!client.servers.includes(pairingSessionServer)) {
105+
client.servers.push(pairingSessionServer);
106+
}
107+
await client.identityManager.claim(identityKey);
108+
if (!cancelled) setApproved(true);
109+
} catch (err) {
110+
if (!cancelled) {
111+
const message =
112+
err instanceof Error ? err.message : 'Failed to claim identity';
113+
setError(message);
114+
}
115+
}
116+
})();
117+
118+
return () => {
119+
cancelled = true;
120+
};
121+
}, [authorized, identityKey, pairingSessionServer, client]);
99122

100123
return {
101124
error,

apps/polycentric/src/features/identity-pairing/screens/PairIdentityIssuerScreen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,8 @@ export default function PairIdentityIssuerScreen() {
271271
pairAsRotationKey,
272272
);
273273
router.back();
274-
} catch {
274+
} catch (err) {
275+
console.error('approve failed:', err);
275276
} finally {
276277
setApprovingClaimers((prev) => {
277278
const next = new Set(prev);

packages/js-core/src/client-internal/pairing-session-manager.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,10 @@ export class PairingSessionManager {
5353
identityKey: string,
5454
server: string,
5555
): Promise<ActivePairingSession> {
56-
// Create and sign the local InitialPairingSession
5756
const pairingSessionBytes = Proto.InitialPairingSession.toBinary(
5857
Proto.InitialPairingSession.create({
5958
issuerIdentity: identityKey,
60-
// Servers don't allow these to be in the future.
61-
// 1 second in the past to prevent timing issues.
62-
createdAt: BigInt(Date.now() - 1_000),
59+
timestamp: BigInt(Date.now()),
6360
}),
6461
);
6562
const signedMessage = await this.signMessage(pairingSessionBytes);
@@ -71,12 +68,11 @@ export class PairingSessionManager {
7168
const session = Proto.PairingSession.fromBinary(
7269
new Uint8Array(sessionBytes),
7370
);
74-
const initialSession = session.initialSession!;
7571

7672
return {
7773
code: bytesToHex(signedMessage.signature),
78-
identityKey: initialSession.issuerIdentity,
79-
createdAt: new Date(Number(initialSession.createdAt)),
74+
identityKey: session.issuerIdentity,
75+
createdAt: new Date(Number(session.createdAt)),
8076
expiresAt: new Date(Number(session.expiresAt)),
8177
signedBy: session.signedBy!,
8278
claimers: [],
@@ -98,13 +94,12 @@ export class PairingSessionManager {
9894
const session = Proto.PairingSession.fromBinary(
9995
new Uint8Array(sessionBytes),
10096
);
101-
const initialSession = session.initialSession!;
10297
return {
10398
session,
10499
claimerPubkeys: [...session.claimerPubkeys],
105100
pairingSession: {
106-
issuerIdentity: initialSession.issuerIdentity,
107-
createdAt: new Date(Number(initialSession.createdAt)),
101+
issuerIdentity: session.issuerIdentity,
102+
createdAt: new Date(Number(session.createdAt)),
108103
expiresAt: new Date(Number(session.expiresAt)),
109104
signedBy: session.signedBy!,
110105
},
@@ -130,13 +125,12 @@ export class PairingSessionManager {
130125
const session = Proto.PairingSession.fromBinary(
131126
new Uint8Array(sessionBytes),
132127
);
133-
const initialSession = session.initialSession!;
134128
return {
135129
session,
136130
claimerPubkeys: [...session.claimerPubkeys],
137131
pairingSession: {
138-
issuerIdentity: initialSession.issuerIdentity,
139-
createdAt: new Date(Number(initialSession.createdAt)),
132+
issuerIdentity: session.issuerIdentity,
133+
createdAt: new Date(Number(session.createdAt)),
140134
expiresAt: new Date(Number(session.expiresAt)),
141135
signedBy: session.signedBy!,
142136
},

packages/js-core/src/polycentric-client.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,12 @@ export class PolycentricClient {
322322
createdAt: BigInt(Date.now()),
323323
});
324324

325-
event.vectorClock = this.buildVectorClock(event);
325+
const identityContentForVC =
326+
collection === COLLECTION.IDENTITY &&
327+
content.contentBody.oneofKind === 'identity'
328+
? content.contentBody.identity
329+
: undefined;
330+
event.vectorClock = this.buildVectorClock(event, identityContentForVC);
326331

327332
return event;
328333
}
@@ -395,13 +400,19 @@ export class PolycentricClient {
395400
* Requires the identity event and its content to already have been
396401
* copied into the core via `copy_events` / `copy_contents`.
397402
*/
398-
buildVectorClock(event: Proto.Event): Proto.VectorClock {
403+
buildVectorClock(
404+
event: Proto.Event,
405+
identityContent?: Proto.Identity,
406+
): Proto.VectorClock {
399407
const clockBytes = this.core.buildVectorClock(
400408
event.key!.identity,
401409
event.key!.collection,
402410
event.identitySequence,
403411
Proto.PublicKey.toBinary(event.key!.signedBy!).buffer as ArrayBuffer,
404412
event.key!.sequence,
413+
identityContent
414+
? (Proto.Identity.toBinary(identityContent).buffer as ArrayBuffer)
415+
: undefined,
405416
);
406417
return Proto.VectorClock.fromBinary(new Uint8Array(clockBytes));
407418
}

packages/js-core/src/proto/polycentric/v2/pairing_service.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ export interface InitialPairingSession {
2727
*/
2828
issuerIdentity: string;
2929
/**
30-
* Creation timestamp in unix milliseconds.
30+
* Timestamp so that the pairing session id is unique.
3131
*
32-
* @generated from protobuf field: int64 created_at = 2
32+
* @generated from protobuf field: int64 timestamp = 2
3333
*/
34-
createdAt: bigint;
34+
timestamp: bigint;
3535
}
3636
/**
3737
* *
@@ -53,7 +53,7 @@ export interface JoinPairingSessionBody {
5353
*/
5454
export interface PairingSession {
5555
/**
56-
* Signature of the signed InitialPairingSession payload.
56+
* Signature of the signed InitialPairingSession payload; serves as the session id.
5757
*
5858
* @generated from protobuf field: string pairing_session_signature = 1
5959
*/
@@ -65,11 +65,11 @@ export interface PairingSession {
6565
*/
6666
signedBy?: PublicKey;
6767
/**
68-
* Session metadata.
68+
* Identity key the session was created for.
6969
*
70-
* @generated from protobuf field: polycentric.v2.InitialPairingSession initial_session = 3
70+
* @generated from protobuf field: string issuer_identity = 3
7171
*/
72-
initialSession?: InitialPairingSession;
72+
issuerIdentity: string;
7373
/**
7474
* Expiration timestamp in unix milliseconds.
7575
*
@@ -82,6 +82,12 @@ export interface PairingSession {
8282
* @generated from protobuf field: repeated polycentric.v2.PublicKey claimer_pubkeys = 5
8383
*/
8484
claimerPubkeys: PublicKey[];
85+
/**
86+
* Server-authoritative creation timestamp in unix milliseconds.
87+
*
88+
* @generated from protobuf field: int64 created_at = 6
89+
*/
90+
createdAt: bigint;
8591
}
8692
/**
8793
* *
@@ -160,13 +166,13 @@ class InitialPairingSession$Type extends MessageType<InitialPairingSession> {
160166
constructor() {
161167
super("polycentric.v2.InitialPairingSession", [
162168
{ no: 1, name: "issuer_identity", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
163-
{ no: 2, name: "created_at", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ }
169+
{ no: 2, name: "timestamp", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ }
164170
]);
165171
}
166172
create(value?: PartialMessage<InitialPairingSession>): InitialPairingSession {
167173
const message = globalThis.Object.create((this.messagePrototype!));
168174
message.issuerIdentity = "";
169-
message.createdAt = 0n;
175+
message.timestamp = 0n;
170176
if (value !== undefined)
171177
reflectionMergePartial<InitialPairingSession>(this, message, value);
172178
return message;
@@ -179,8 +185,8 @@ class InitialPairingSession$Type extends MessageType<InitialPairingSession> {
179185
case /* string issuer_identity */ 1:
180186
message.issuerIdentity = reader.string();
181187
break;
182-
case /* int64 created_at */ 2:
183-
message.createdAt = reader.int64().toBigInt();
188+
case /* int64 timestamp */ 2:
189+
message.timestamp = reader.int64().toBigInt();
184190
break;
185191
default:
186192
let u = options.readUnknownField;
@@ -197,9 +203,9 @@ class InitialPairingSession$Type extends MessageType<InitialPairingSession> {
197203
/* string issuer_identity = 1; */
198204
if (message.issuerIdentity !== "")
199205
writer.tag(1, WireType.LengthDelimited).string(message.issuerIdentity);
200-
/* int64 created_at = 2; */
201-
if (message.createdAt !== 0n)
202-
writer.tag(2, WireType.Varint).int64(message.createdAt);
206+
/* int64 timestamp = 2; */
207+
if (message.timestamp !== 0n)
208+
writer.tag(2, WireType.Varint).int64(message.timestamp);
203209
let u = options.writeUnknownFields;
204210
if (u !== false)
205211
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -263,16 +269,19 @@ class PairingSession$Type extends MessageType<PairingSession> {
263269
super("polycentric.v2.PairingSession", [
264270
{ no: 1, name: "pairing_session_signature", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
265271
{ no: 2, name: "signed_by", kind: "message", T: () => PublicKey },
266-
{ no: 3, name: "initial_session", kind: "message", T: () => InitialPairingSession },
272+
{ no: 3, name: "issuer_identity", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
267273
{ no: 4, name: "expires_at", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ },
268-
{ no: 5, name: "claimer_pubkeys", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => PublicKey }
274+
{ no: 5, name: "claimer_pubkeys", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => PublicKey },
275+
{ no: 6, name: "created_at", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ }
269276
]);
270277
}
271278
create(value?: PartialMessage<PairingSession>): PairingSession {
272279
const message = globalThis.Object.create((this.messagePrototype!));
273280
message.pairingSessionSignature = "";
281+
message.issuerIdentity = "";
274282
message.expiresAt = 0n;
275283
message.claimerPubkeys = [];
284+
message.createdAt = 0n;
276285
if (value !== undefined)
277286
reflectionMergePartial<PairingSession>(this, message, value);
278287
return message;
@@ -288,15 +297,18 @@ class PairingSession$Type extends MessageType<PairingSession> {
288297
case /* polycentric.v2.PublicKey signed_by */ 2:
289298
message.signedBy = PublicKey.internalBinaryRead(reader, reader.uint32(), options, message.signedBy);
290299
break;
291-
case /* polycentric.v2.InitialPairingSession initial_session */ 3:
292-
message.initialSession = InitialPairingSession.internalBinaryRead(reader, reader.uint32(), options, message.initialSession);
300+
case /* string issuer_identity */ 3:
301+
message.issuerIdentity = reader.string();
293302
break;
294303
case /* int64 expires_at */ 4:
295304
message.expiresAt = reader.int64().toBigInt();
296305
break;
297306
case /* repeated polycentric.v2.PublicKey claimer_pubkeys */ 5:
298307
message.claimerPubkeys.push(PublicKey.internalBinaryRead(reader, reader.uint32(), options));
299308
break;
309+
case /* int64 created_at */ 6:
310+
message.createdAt = reader.int64().toBigInt();
311+
break;
300312
default:
301313
let u = options.readUnknownField;
302314
if (u === "throw")
@@ -315,15 +327,18 @@ class PairingSession$Type extends MessageType<PairingSession> {
315327
/* polycentric.v2.PublicKey signed_by = 2; */
316328
if (message.signedBy)
317329
PublicKey.internalBinaryWrite(message.signedBy, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
318-
/* polycentric.v2.InitialPairingSession initial_session = 3; */
319-
if (message.initialSession)
320-
InitialPairingSession.internalBinaryWrite(message.initialSession, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
330+
/* string issuer_identity = 3; */
331+
if (message.issuerIdentity !== "")
332+
writer.tag(3, WireType.LengthDelimited).string(message.issuerIdentity);
321333
/* int64 expires_at = 4; */
322334
if (message.expiresAt !== 0n)
323335
writer.tag(4, WireType.Varint).int64(message.expiresAt);
324336
/* repeated polycentric.v2.PublicKey claimer_pubkeys = 5; */
325337
for (let i = 0; i < message.claimerPubkeys.length; i++)
326338
PublicKey.internalBinaryWrite(message.claimerPubkeys[i], writer.tag(5, WireType.LengthDelimited).fork(), options).join();
339+
/* int64 created_at = 6; */
340+
if (message.createdAt !== 0n)
341+
writer.tag(6, WireType.Varint).int64(message.createdAt);
327342
let u = options.writeUnknownFields;
328343
if (u !== false)
329344
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);

0 commit comments

Comments
 (0)