@bcts/gstp 1.0.0-alpha.14
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/LICENSE +48 -0
- package/README.md +17 -0
- package/dist/chunk-15K8U1wQ.mjs +18 -0
- package/dist/index.cjs +1308 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +853 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +851 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +1308 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +1280 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +92 -0
- package/src/continuation.ts +334 -0
- package/src/error.ts +145 -0
- package/src/index.ts +30 -0
- package/src/prelude.ts +24 -0
- package/src/sealed-event.ts +479 -0
- package/src/sealed-request.ts +580 -0
- package/src/sealed-response.ts +507 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SealedRequest - Sealed request messages for GSTP
|
|
3
|
+
*
|
|
4
|
+
* A SealedRequest wraps a Request with sender information and state
|
|
5
|
+
* continuations for secure, authenticated request messages.
|
|
6
|
+
*
|
|
7
|
+
* Ported from gstp-rust/src/sealed_request.rs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ARID, PrivateKeys, Signer } from "@bcts/components";
|
|
11
|
+
import {
|
|
12
|
+
Envelope,
|
|
13
|
+
Request,
|
|
14
|
+
type Expression,
|
|
15
|
+
type Function,
|
|
16
|
+
type EnvelopeEncodableValue,
|
|
17
|
+
type ParameterID,
|
|
18
|
+
} from "@bcts/envelope";
|
|
19
|
+
import { SENDER, SENDER_CONTINUATION, RECIPIENT_CONTINUATION } from "@bcts/known-values";
|
|
20
|
+
import { XIDDocument } from "@bcts/xid";
|
|
21
|
+
import { Continuation } from "./continuation";
|
|
22
|
+
import { GstpError } from "./error";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Interface that defines the behavior of a sealed request.
|
|
26
|
+
*
|
|
27
|
+
* Extends RequestBehavior with additional methods for managing
|
|
28
|
+
* sender information and state continuations.
|
|
29
|
+
*/
|
|
30
|
+
export interface SealedRequestBehavior {
|
|
31
|
+
/**
|
|
32
|
+
* Adds state to the request that the receiver must return in the response.
|
|
33
|
+
*/
|
|
34
|
+
withState(state: EnvelopeEncodableValue): SealedRequest;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adds optional state to the request.
|
|
38
|
+
*/
|
|
39
|
+
withOptionalState(state: EnvelopeEncodableValue | undefined): SealedRequest;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Adds a continuation previously received from the recipient.
|
|
43
|
+
*/
|
|
44
|
+
withPeerContinuation(peerContinuation: Envelope): SealedRequest;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adds an optional continuation previously received from the recipient.
|
|
48
|
+
*/
|
|
49
|
+
withOptionalPeerContinuation(peerContinuation: Envelope | undefined): SealedRequest;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns the underlying request.
|
|
53
|
+
*/
|
|
54
|
+
request(): Request;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the sender of the request.
|
|
58
|
+
*/
|
|
59
|
+
sender(): XIDDocument;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the state to be sent to the recipient.
|
|
63
|
+
*/
|
|
64
|
+
state(): Envelope | undefined;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the continuation received from the recipient.
|
|
68
|
+
*/
|
|
69
|
+
peerContinuation(): Envelope | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A sealed request that combines a Request with sender information and
|
|
74
|
+
* state continuations for secure communication.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* import { SealedRequest, ARID } from '@bcts/gstp';
|
|
79
|
+
* import { XIDDocument } from '@bcts/xid';
|
|
80
|
+
*
|
|
81
|
+
* // Create sender XID document
|
|
82
|
+
* const sender = XIDDocument.new();
|
|
83
|
+
* const requestId = ARID.new();
|
|
84
|
+
*
|
|
85
|
+
* // Create a sealed request
|
|
86
|
+
* const request = SealedRequest.new("getBalance", requestId, sender)
|
|
87
|
+
* .withParameter("account", "alice")
|
|
88
|
+
* .withState("session-state-data")
|
|
89
|
+
* .withNote("Balance check");
|
|
90
|
+
*
|
|
91
|
+
* // Convert to sealed envelope
|
|
92
|
+
* const envelope = request.toEnvelope(
|
|
93
|
+
* new Date(Date.now() + 60000), // Valid for 60 seconds
|
|
94
|
+
* senderPrivateKey,
|
|
95
|
+
* recipientXIDDocument
|
|
96
|
+
* );
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export class SealedRequest implements SealedRequestBehavior {
|
|
100
|
+
private _request: Request;
|
|
101
|
+
private readonly _sender: XIDDocument;
|
|
102
|
+
private _state: Envelope | undefined;
|
|
103
|
+
private _peerContinuation: Envelope | undefined;
|
|
104
|
+
|
|
105
|
+
private constructor(
|
|
106
|
+
request: Request,
|
|
107
|
+
sender: XIDDocument,
|
|
108
|
+
state?: Envelope,
|
|
109
|
+
peerContinuation?: Envelope,
|
|
110
|
+
) {
|
|
111
|
+
this._request = request;
|
|
112
|
+
this._sender = sender;
|
|
113
|
+
this._state = state;
|
|
114
|
+
this._peerContinuation = peerContinuation;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a new sealed request with the given function, ID, and sender.
|
|
119
|
+
*
|
|
120
|
+
* @param func - The function to call (string name or Function object)
|
|
121
|
+
* @param id - The request ID
|
|
122
|
+
* @param sender - The sender's XID document
|
|
123
|
+
*/
|
|
124
|
+
static new(func: string | number | Function, id: ARID, sender: XIDDocument): SealedRequest {
|
|
125
|
+
return new SealedRequest(Request.new(func, id), sender);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates a new sealed request with an expression body.
|
|
130
|
+
*
|
|
131
|
+
* @param body - The expression body
|
|
132
|
+
* @param id - The request ID
|
|
133
|
+
* @param sender - The sender's XID document
|
|
134
|
+
*/
|
|
135
|
+
static newWithBody(body: Expression, id: ARID, sender: XIDDocument): SealedRequest {
|
|
136
|
+
return new SealedRequest(Request.newWithBody(body, id), sender);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// ExpressionBehavior implementation
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Adds a parameter to the request.
|
|
145
|
+
*/
|
|
146
|
+
withParameter(parameter: ParameterID, value: EnvelopeEncodableValue): SealedRequest {
|
|
147
|
+
this._request = this._request.withParameter(parameter, value);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Adds an optional parameter to the request.
|
|
153
|
+
*/
|
|
154
|
+
withOptionalParameter(
|
|
155
|
+
parameter: ParameterID,
|
|
156
|
+
value: EnvelopeEncodableValue | undefined,
|
|
157
|
+
): SealedRequest {
|
|
158
|
+
if (value !== undefined) {
|
|
159
|
+
this._request = this._request.withParameter(parameter, value);
|
|
160
|
+
}
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Returns the function of the request.
|
|
166
|
+
*/
|
|
167
|
+
function(): Function {
|
|
168
|
+
return this._request.function();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Returns the expression envelope of the request.
|
|
173
|
+
*/
|
|
174
|
+
expressionEnvelope(): Envelope {
|
|
175
|
+
return this._request.expressionEnvelope();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns the object for a parameter.
|
|
180
|
+
*/
|
|
181
|
+
objectForParameter(param: ParameterID): Envelope | undefined {
|
|
182
|
+
return this._request.body().getParameter(param);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns all objects for a parameter.
|
|
187
|
+
*/
|
|
188
|
+
objectsForParameter(param: ParameterID): Envelope[] {
|
|
189
|
+
const obj = this._request.body().getParameter(param);
|
|
190
|
+
return obj !== undefined ? [obj] : [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Extracts an object for a parameter as a specific type.
|
|
195
|
+
*/
|
|
196
|
+
extractObjectForParameter<T>(param: ParameterID): T {
|
|
197
|
+
const envelope = this.objectForParameter(param);
|
|
198
|
+
if (envelope === undefined) {
|
|
199
|
+
throw GstpError.envelope(new Error(`Parameter not found: ${param}`));
|
|
200
|
+
}
|
|
201
|
+
return envelope.extractSubject((cbor) => {
|
|
202
|
+
// Extract primitive value from CBOR
|
|
203
|
+
if (cbor.isInteger()) return cbor.toInteger() as T;
|
|
204
|
+
if (cbor.isText()) return cbor.toText() as T;
|
|
205
|
+
if (cbor.isBool()) return cbor.toBool() as T;
|
|
206
|
+
if (cbor.isNumber()) return cbor.toNumber() as T;
|
|
207
|
+
if (cbor.isByteString()) return cbor.toByteString() as T;
|
|
208
|
+
return cbor as T;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Extracts an optional object for a parameter.
|
|
214
|
+
*/
|
|
215
|
+
extractOptionalObjectForParameter<T>(param: ParameterID): T | undefined {
|
|
216
|
+
const envelope = this.objectForParameter(param);
|
|
217
|
+
if (envelope === undefined) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return envelope.extractSubject((cbor) => {
|
|
221
|
+
// Extract primitive value from CBOR
|
|
222
|
+
if (cbor.isInteger()) return cbor.toInteger() as T;
|
|
223
|
+
if (cbor.isText()) return cbor.toText() as T;
|
|
224
|
+
if (cbor.isBool()) return cbor.toBool() as T;
|
|
225
|
+
if (cbor.isNumber()) return cbor.toNumber() as T;
|
|
226
|
+
if (cbor.isByteString()) return cbor.toByteString() as T;
|
|
227
|
+
return cbor as T;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extracts all objects for a parameter as a specific type.
|
|
233
|
+
*/
|
|
234
|
+
extractObjectsForParameter<T>(param: ParameterID): T[] {
|
|
235
|
+
return this.objectsForParameter(param).map((env) => env.extractSubject((cbor) => cbor as T));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// RequestBehavior implementation
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Adds a note to the request.
|
|
244
|
+
*/
|
|
245
|
+
withNote(note: string): SealedRequest {
|
|
246
|
+
this._request = this._request.withNote(note);
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Adds a date to the request.
|
|
252
|
+
*/
|
|
253
|
+
withDate(date: Date): SealedRequest {
|
|
254
|
+
this._request = this._request.withDate(date);
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Returns the body of the request.
|
|
260
|
+
*/
|
|
261
|
+
body(): Expression {
|
|
262
|
+
return this._request.body();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Returns the ID of the request.
|
|
267
|
+
*/
|
|
268
|
+
id(): ARID {
|
|
269
|
+
return this._request.id();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Returns the note of the request.
|
|
274
|
+
*/
|
|
275
|
+
note(): string {
|
|
276
|
+
return this._request.note();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Returns the date of the request.
|
|
281
|
+
*/
|
|
282
|
+
date(): Date | undefined {
|
|
283
|
+
return this._request.date();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// SealedRequestBehavior implementation
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Adds state to the request that the receiver must return in the response.
|
|
292
|
+
*/
|
|
293
|
+
withState(state: EnvelopeEncodableValue): SealedRequest {
|
|
294
|
+
this._state = Envelope.new(state);
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Adds optional state to the request.
|
|
300
|
+
*/
|
|
301
|
+
withOptionalState(state: EnvelopeEncodableValue | undefined): SealedRequest {
|
|
302
|
+
this._state = state !== undefined ? Envelope.new(state) : undefined;
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Adds a continuation previously received from the recipient.
|
|
308
|
+
*/
|
|
309
|
+
withPeerContinuation(peerContinuation: Envelope): SealedRequest {
|
|
310
|
+
this._peerContinuation = peerContinuation;
|
|
311
|
+
return this;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Adds an optional continuation previously received from the recipient.
|
|
316
|
+
*/
|
|
317
|
+
withOptionalPeerContinuation(peerContinuation: Envelope | undefined): SealedRequest {
|
|
318
|
+
this._peerContinuation = peerContinuation;
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Returns the underlying request.
|
|
324
|
+
*/
|
|
325
|
+
request(): Request {
|
|
326
|
+
return this._request;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Returns the sender of the request.
|
|
331
|
+
*/
|
|
332
|
+
sender(): XIDDocument {
|
|
333
|
+
return this._sender;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Returns the state to be sent to the recipient.
|
|
338
|
+
*/
|
|
339
|
+
state(): Envelope | undefined {
|
|
340
|
+
return this._state;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Returns the continuation received from the recipient.
|
|
345
|
+
*/
|
|
346
|
+
peerContinuation(): Envelope | undefined {
|
|
347
|
+
return this._peerContinuation;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============================================================================
|
|
351
|
+
// Conversion methods
|
|
352
|
+
// ============================================================================
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Converts the sealed request to a Request.
|
|
356
|
+
*/
|
|
357
|
+
toRequest(): Request {
|
|
358
|
+
return this._request;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Converts the sealed request to an Expression.
|
|
363
|
+
*/
|
|
364
|
+
toExpression(): Expression {
|
|
365
|
+
return this._request.body();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// Envelope methods
|
|
370
|
+
// ============================================================================
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Creates an envelope that can be decrypted by zero or one recipient.
|
|
374
|
+
*
|
|
375
|
+
* @param validUntil - Optional expiration date for the continuation
|
|
376
|
+
* @param signer - Optional signer for the envelope
|
|
377
|
+
* @param recipient - Optional recipient XID document for encryption
|
|
378
|
+
* @returns The sealed request as an envelope
|
|
379
|
+
*/
|
|
380
|
+
toEnvelope(validUntil?: Date, signer?: Signer, recipient?: XIDDocument): Envelope {
|
|
381
|
+
const recipients: XIDDocument[] = recipient !== undefined ? [recipient] : [];
|
|
382
|
+
return this.toEnvelopeForRecipients(validUntil, signer, recipients);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Creates an envelope that can be decrypted by zero or more recipients.
|
|
387
|
+
*
|
|
388
|
+
* @param validUntil - Optional expiration date for the continuation
|
|
389
|
+
* @param signer - Optional signer for the envelope
|
|
390
|
+
* @param recipients - Array of recipient XID documents for encryption
|
|
391
|
+
* @returns The sealed request as an envelope
|
|
392
|
+
*/
|
|
393
|
+
toEnvelopeForRecipients(
|
|
394
|
+
validUntil?: Date,
|
|
395
|
+
signer?: Signer,
|
|
396
|
+
recipients?: XIDDocument[],
|
|
397
|
+
): Envelope {
|
|
398
|
+
// Even if no state is provided, requests always include a continuation
|
|
399
|
+
// that at least specifies the required valid response ID.
|
|
400
|
+
const stateEnvelope = this._state ?? Envelope.new(null);
|
|
401
|
+
const continuation = new Continuation(stateEnvelope, this.id(), validUntil);
|
|
402
|
+
|
|
403
|
+
// Get sender's encryption key (from inception key)
|
|
404
|
+
const senderInceptionKey = this._sender.inceptionKey();
|
|
405
|
+
const senderEncryptionKey = senderInceptionKey?.publicKeys()?.encapsulationPublicKey();
|
|
406
|
+
if (senderEncryptionKey === undefined) {
|
|
407
|
+
throw GstpError.senderMissingEncryptionKey();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Create sender continuation (encrypted to sender)
|
|
411
|
+
const senderContinuation = continuation.toEnvelope(senderEncryptionKey);
|
|
412
|
+
|
|
413
|
+
// Build the request envelope
|
|
414
|
+
let result = this._request.toEnvelope();
|
|
415
|
+
|
|
416
|
+
// Add sender assertion
|
|
417
|
+
result = result.addAssertion(SENDER, this._sender.toEnvelope());
|
|
418
|
+
|
|
419
|
+
// Add sender continuation
|
|
420
|
+
result = result.addAssertion(SENDER_CONTINUATION, senderContinuation);
|
|
421
|
+
|
|
422
|
+
// Add peer continuation if present
|
|
423
|
+
if (this._peerContinuation !== undefined) {
|
|
424
|
+
result = result.addAssertion(RECIPIENT_CONTINUATION, this._peerContinuation);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Sign if signer provided (sign() wraps first, then adds signature)
|
|
428
|
+
if (signer !== undefined) {
|
|
429
|
+
result = result.sign(signer);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Encrypt to recipients if provided
|
|
433
|
+
if (recipients !== undefined && recipients.length > 0) {
|
|
434
|
+
const recipientKeys = recipients.map((recipient) => {
|
|
435
|
+
const key = recipient.encryptionKey();
|
|
436
|
+
if (key === undefined) {
|
|
437
|
+
throw GstpError.recipientMissingEncryptionKey();
|
|
438
|
+
}
|
|
439
|
+
return key;
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
result = result.wrap().encryptSubjectToRecipients(recipientKeys);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Parses a sealed request from an encrypted envelope.
|
|
450
|
+
*
|
|
451
|
+
* @param encryptedEnvelope - The encrypted envelope to parse
|
|
452
|
+
* @param expectedId - Optional expected request ID for validation
|
|
453
|
+
* @param now - Optional current time for continuation validation
|
|
454
|
+
* @param recipient - The recipient's private keys for decryption
|
|
455
|
+
* @returns The parsed sealed request
|
|
456
|
+
*/
|
|
457
|
+
static tryFromEnvelope(
|
|
458
|
+
encryptedEnvelope: Envelope,
|
|
459
|
+
expectedId: ARID | undefined,
|
|
460
|
+
now: Date | undefined,
|
|
461
|
+
recipient: PrivateKeys,
|
|
462
|
+
): SealedRequest {
|
|
463
|
+
// Decrypt the envelope
|
|
464
|
+
let signedEnvelope: Envelope;
|
|
465
|
+
try {
|
|
466
|
+
signedEnvelope = encryptedEnvelope.decryptToRecipient(recipient);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
throw GstpError.envelope(e instanceof Error ? e : new Error(String(e)));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Extract sender from the unwrapped envelope
|
|
472
|
+
let sender: XIDDocument;
|
|
473
|
+
try {
|
|
474
|
+
const unwrapped = signedEnvelope.tryUnwrap();
|
|
475
|
+
const senderEnvelope = unwrapped.objectForPredicate(SENDER);
|
|
476
|
+
if (senderEnvelope === undefined) {
|
|
477
|
+
throw new Error("Missing sender");
|
|
478
|
+
}
|
|
479
|
+
sender = XIDDocument.fromEnvelope(senderEnvelope);
|
|
480
|
+
} catch (e) {
|
|
481
|
+
throw GstpError.xid(e instanceof Error ? e : new Error(String(e)));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Get sender's verification key and verify signature (from inception key)
|
|
485
|
+
const senderInceptionKey = sender.inceptionKey();
|
|
486
|
+
const senderVerificationKey = senderInceptionKey?.publicKeys()?.signingPublicKey();
|
|
487
|
+
if (senderVerificationKey === undefined) {
|
|
488
|
+
throw GstpError.senderMissingVerificationKey();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let requestEnvelope: Envelope;
|
|
492
|
+
try {
|
|
493
|
+
// verify() both verifies the signature AND unwraps the envelope
|
|
494
|
+
requestEnvelope = signedEnvelope.verify(senderVerificationKey);
|
|
495
|
+
} catch (e) {
|
|
496
|
+
throw GstpError.envelope(e instanceof Error ? e : new Error(String(e)));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Get peer continuation (sender_continuation from the request)
|
|
500
|
+
const peerContinuation = requestEnvelope.optionalObjectForPredicate(SENDER_CONTINUATION);
|
|
501
|
+
if (peerContinuation !== undefined) {
|
|
502
|
+
// Verify peer continuation is encrypted
|
|
503
|
+
if (!peerContinuation.subject().isEncrypted()) {
|
|
504
|
+
throw GstpError.peerContinuationNotEncrypted();
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
throw GstpError.missingPeerContinuation();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Get and decrypt our continuation (recipient_continuation)
|
|
511
|
+
const encryptedContinuation =
|
|
512
|
+
requestEnvelope.optionalObjectForPredicate(RECIPIENT_CONTINUATION);
|
|
513
|
+
let state: Envelope | undefined;
|
|
514
|
+
if (encryptedContinuation !== undefined) {
|
|
515
|
+
const continuation = Continuation.tryFromEnvelope(
|
|
516
|
+
encryptedContinuation,
|
|
517
|
+
expectedId,
|
|
518
|
+
now,
|
|
519
|
+
recipient,
|
|
520
|
+
);
|
|
521
|
+
state = continuation.state();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Parse the request
|
|
525
|
+
let request: Request;
|
|
526
|
+
try {
|
|
527
|
+
request = Request.fromEnvelope(requestEnvelope);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
throw GstpError.envelope(e instanceof Error ? e : new Error(String(e)));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return new SealedRequest(request, sender, state, peerContinuation);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ============================================================================
|
|
536
|
+
// Display methods
|
|
537
|
+
// ============================================================================
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Returns a string representation of the sealed request.
|
|
541
|
+
*/
|
|
542
|
+
toString(): string {
|
|
543
|
+
const stateStr = this._state !== undefined ? this._state.formatFlat() : "None";
|
|
544
|
+
const peerStr = this._peerContinuation !== undefined ? "Some" : "None";
|
|
545
|
+
return `SealedRequest(${this._request.summary()}, state: ${stateStr}, peer_continuation: ${peerStr})`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Checks equality with another sealed request.
|
|
550
|
+
*/
|
|
551
|
+
equals(other: SealedRequest): boolean {
|
|
552
|
+
if (!this._request.equals(other._request)) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
if (!this._sender.xid().equals(other._sender.xid())) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
// Compare state
|
|
559
|
+
if (this._state === undefined && other._state === undefined) {
|
|
560
|
+
// Both undefined, equal
|
|
561
|
+
} else if (this._state !== undefined && other._state !== undefined) {
|
|
562
|
+
if (!this._state.digest().equals(other._state.digest())) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
// Compare peer continuation
|
|
569
|
+
if (this._peerContinuation === undefined && other._peerContinuation === undefined) {
|
|
570
|
+
// Both undefined, equal
|
|
571
|
+
} else if (this._peerContinuation !== undefined && other._peerContinuation !== undefined) {
|
|
572
|
+
if (!this._peerContinuation.digest().equals(other._peerContinuation.digest())) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
}
|