jazz-tools 0.15.15 → 0.15.16
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/.turbo/turbo-build.log +36 -40
- package/CHANGELOG.md +10 -0
- package/dist/{chunk-4CFNXQE7.js → chunk-OSVAAVWQ.js} +103 -6
- package/dist/chunk-OSVAAVWQ.js.map +1 -0
- package/dist/index.js +357 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/testing.js +3 -1
- package/dist/react/testing.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/group.d.ts +1 -0
- package/dist/tools/coValues/group.d.ts.map +1 -1
- package/dist/tools/coValues/inbox.d.ts.map +1 -1
- package/dist/tools/coValues/interfaces.d.ts +58 -2
- package/dist/tools/coValues/interfaces.d.ts.map +1 -1
- package/dist/tools/coValues/request.d.ts +82 -0
- package/dist/tools/coValues/request.d.ts.map +1 -0
- package/dist/tools/exports.d.ts +2 -1
- package/dist/tools/exports.d.ts.map +1 -1
- package/dist/tools/lib/id.d.ts +2 -0
- package/dist/tools/lib/id.d.ts.map +1 -0
- package/dist/tools/subscribe/SubscriptionScope.d.ts +3 -2
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/dist/tools/tests/exportImport.test.d.ts +2 -0
- package/dist/tools/tests/exportImport.test.d.ts.map +1 -0
- package/dist/tools/tests/request.test.d.ts +2 -0
- package/dist/tools/tests/request.test.d.ts.map +1 -0
- package/package.json +6 -5
- package/src/tools/coValues/group.ts +1 -0
- package/src/tools/coValues/inbox.ts +4 -3
- package/src/tools/coValues/interfaces.ts +177 -2
- package/src/tools/coValues/request.ts +631 -0
- package/src/tools/exports.ts +8 -0
- package/src/tools/lib/id.ts +3 -0
- package/src/tools/subscribe/SubscriptionScope.ts +10 -1
- package/src/tools/tests/exportImport.test.ts +526 -0
- package/src/tools/tests/request.test.ts +951 -0
- package/tsup.config.ts +0 -2
- package/dist/chunk-4CFNXQE7.js.map +0 -1
@@ -0,0 +1,631 @@
|
|
1
|
+
import {
|
2
|
+
CoValueCore,
|
3
|
+
CojsonInternalTypes,
|
4
|
+
CryptoProvider,
|
5
|
+
RawAccount,
|
6
|
+
RawCoMap,
|
7
|
+
cojsonInternals,
|
8
|
+
} from "cojson";
|
9
|
+
import z from "zod/v4";
|
10
|
+
import {
|
11
|
+
AnyCoMapSchema,
|
12
|
+
AnyCoSchema,
|
13
|
+
CoMap,
|
14
|
+
CoMapInitZod,
|
15
|
+
CoMapSchema,
|
16
|
+
CoValueClass,
|
17
|
+
Group,
|
18
|
+
Loaded,
|
19
|
+
ResolveQuery,
|
20
|
+
ResolveQueryStrict,
|
21
|
+
Simplify,
|
22
|
+
anySchemaToCoSchema,
|
23
|
+
coMapDefiner,
|
24
|
+
exportCoValue,
|
25
|
+
importContentPieces,
|
26
|
+
loadCoValue,
|
27
|
+
} from "../internal.js";
|
28
|
+
import { isCoValueId } from "../lib/id.js";
|
29
|
+
import { Account } from "./account.js";
|
30
|
+
|
31
|
+
type MessageShape = Record<string, z.core.$ZodType | AnyCoSchema>;
|
32
|
+
|
33
|
+
type RequestSchemaDefinition<
|
34
|
+
S extends MessageShape,
|
35
|
+
R extends ResolveQuery<CoMapSchema<S>> = true,
|
36
|
+
> =
|
37
|
+
| S
|
38
|
+
| {
|
39
|
+
schema: S;
|
40
|
+
resolve?: R;
|
41
|
+
};
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Configuration options for defining HTTP request/response schemas in Jazz.
|
45
|
+
*
|
46
|
+
* This interface defines the structure for creating typed HTTP routes with
|
47
|
+
* request and response validation using CoMap schemas.
|
48
|
+
*
|
49
|
+
* @template RequestShape - The shape of the request message schema (must extend MessageShape)
|
50
|
+
* @template RequestResolve - The resolve query type for the request CoMap schema
|
51
|
+
* @template ResponseShape - The shape of the response message schema (must extend MessageShape)
|
52
|
+
* @template ResponseResolve - The resolve query type for the response CoMap schema
|
53
|
+
*/
|
54
|
+
interface RequestOptions<
|
55
|
+
RequestShape extends MessageShape,
|
56
|
+
RequestResolve extends ResolveQuery<CoMapSchema<RequestShape>>,
|
57
|
+
ResponseShape extends MessageShape,
|
58
|
+
ResponseResolve extends ResolveQuery<CoMapSchema<ResponseShape>>,
|
59
|
+
> {
|
60
|
+
/**
|
61
|
+
* The URL endpoint for the HTTP route.
|
62
|
+
* This is used by the client to send requests to the server.
|
63
|
+
*/
|
64
|
+
url: string;
|
65
|
+
|
66
|
+
/**
|
67
|
+
* The id of the worker Account or Group.
|
68
|
+
*/
|
69
|
+
workerId: string;
|
70
|
+
|
71
|
+
/**
|
72
|
+
* Schema definition for the request payload.
|
73
|
+
* Can be either a direct schema object or an object with schema and optional resolve properties.
|
74
|
+
* The schema defines the structure and validation rules for incoming requests.
|
75
|
+
*/
|
76
|
+
request: RequestSchemaDefinition<
|
77
|
+
RequestShape,
|
78
|
+
ResolveQueryStrict<CoMapSchema<RequestShape>, RequestResolve>
|
79
|
+
>;
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Schema definition for the response payload.
|
83
|
+
* Can be either a direct schema object or an object with schema and optional resolve properties.
|
84
|
+
* The schema defines the structure and validation rules for outgoing responses.
|
85
|
+
*/
|
86
|
+
response: RequestSchemaDefinition<
|
87
|
+
ResponseShape,
|
88
|
+
ResolveQueryStrict<CoMapSchema<ResponseShape>, ResponseResolve>
|
89
|
+
>;
|
90
|
+
}
|
91
|
+
|
92
|
+
type AsNullablePayload<T extends MessageShape> = T extends Record<string, never>
|
93
|
+
? undefined
|
94
|
+
: never;
|
95
|
+
type MessageValuePayload<T extends MessageShape> =
|
96
|
+
| Simplify<CoMapInitZod<T>>
|
97
|
+
| AsNullablePayload<T>;
|
98
|
+
|
99
|
+
function createMessageEnvelope<S extends MessageShape>(
|
100
|
+
schema: AnyCoMapSchema,
|
101
|
+
value: MessageValuePayload<S>,
|
102
|
+
owner: Account,
|
103
|
+
sharedWith: Account | Group,
|
104
|
+
type: "request" | "response",
|
105
|
+
): Loaded<CoMapSchema<S>> {
|
106
|
+
const group = Group.create({ owner });
|
107
|
+
|
108
|
+
if (type === "request") {
|
109
|
+
group.addMember(sharedWith, "writer");
|
110
|
+
} else {
|
111
|
+
group.addMember(sharedWith, "reader");
|
112
|
+
}
|
113
|
+
|
114
|
+
// @ts-expect-error - AnyCoMapSchema doesn't have static methods
|
115
|
+
return schema.create(value ?? {}, group);
|
116
|
+
}
|
117
|
+
|
118
|
+
/**
|
119
|
+
* Function that exports the input CoValue in a serializable format and prepares the information
|
120
|
+
* required for the other side to safely verify the identity of the sender.
|
121
|
+
*/
|
122
|
+
async function serializeMessagePayload({
|
123
|
+
type,
|
124
|
+
schema,
|
125
|
+
resolve,
|
126
|
+
value,
|
127
|
+
owner,
|
128
|
+
target,
|
129
|
+
}: {
|
130
|
+
// Skipping type validation here to avoid excessive type complexity that affects the typecheck performance
|
131
|
+
type: "request" | "response";
|
132
|
+
schema: AnyCoMapSchema;
|
133
|
+
resolve: any;
|
134
|
+
value: any;
|
135
|
+
owner: Account;
|
136
|
+
target: Account | Group;
|
137
|
+
}) {
|
138
|
+
const me = owner ?? Account.getMe();
|
139
|
+
const node = me._raw.core.node;
|
140
|
+
const crypto = node.crypto;
|
141
|
+
|
142
|
+
const agent = node.getCurrentAgent();
|
143
|
+
const signerID = agent.currentSignerID();
|
144
|
+
const signerSecret = agent.currentSignerSecret();
|
145
|
+
|
146
|
+
const envelope = createMessageEnvelope(schema, value, me, target, type);
|
147
|
+
|
148
|
+
const contentPieces =
|
149
|
+
(await exportCoValue(schema, envelope.id, {
|
150
|
+
resolve,
|
151
|
+
loadAs: me,
|
152
|
+
bestEffortResolution: true,
|
153
|
+
})) ?? [];
|
154
|
+
|
155
|
+
const createdAt = Date.now();
|
156
|
+
|
157
|
+
const signPayload = crypto.secureHash({
|
158
|
+
contentPieces,
|
159
|
+
id: envelope.id,
|
160
|
+
createdAt,
|
161
|
+
signerID,
|
162
|
+
});
|
163
|
+
|
164
|
+
const authToken = crypto.sign(signerSecret, signPayload);
|
165
|
+
|
166
|
+
return {
|
167
|
+
contentPieces,
|
168
|
+
id: envelope.id,
|
169
|
+
createdAt,
|
170
|
+
authToken,
|
171
|
+
signerID,
|
172
|
+
};
|
173
|
+
}
|
174
|
+
|
175
|
+
const requestSchema = z.object({
|
176
|
+
contentPieces: z.array(z.json()),
|
177
|
+
id: z.custom<`co_z${string}`>(isCoValueId),
|
178
|
+
createdAt: z.number(),
|
179
|
+
authToken: z.custom<`signature_z${string}`>(
|
180
|
+
(value) => typeof value === "string" && value.startsWith("signature_z"),
|
181
|
+
),
|
182
|
+
signerID: z.custom<`signer_z${string}`>(
|
183
|
+
(value) => typeof value === "string" && value.startsWith("signer_z"),
|
184
|
+
),
|
185
|
+
});
|
186
|
+
|
187
|
+
/**
|
188
|
+
* Function that parses the message payload, verifies the identity of the sender and loads the data.
|
189
|
+
*
|
190
|
+
* @returns The data from the message.
|
191
|
+
*/
|
192
|
+
async function handleMessagePayload({
|
193
|
+
type,
|
194
|
+
schema,
|
195
|
+
resolve,
|
196
|
+
request,
|
197
|
+
loadAs,
|
198
|
+
}: {
|
199
|
+
type: "request" | "response";
|
200
|
+
// Skipping type validation here to avoid excessive type complexity that affects the typecheck performance
|
201
|
+
schema: AnyCoMapSchema;
|
202
|
+
resolve: any;
|
203
|
+
request: unknown;
|
204
|
+
loadAs: Account;
|
205
|
+
}) {
|
206
|
+
const node = loadAs._raw.core.node;
|
207
|
+
const crypto = node.crypto;
|
208
|
+
|
209
|
+
const requestParsed = requestSchema.safeParse(request);
|
210
|
+
|
211
|
+
if (!requestParsed.success) {
|
212
|
+
throw new JazzRequestError(
|
213
|
+
"Request payload is not valid",
|
214
|
+
400,
|
215
|
+
requestParsed.error,
|
216
|
+
);
|
217
|
+
}
|
218
|
+
|
219
|
+
const requestData = requestParsed.data;
|
220
|
+
|
221
|
+
if (type === "request") {
|
222
|
+
const core = await node.loadCoValueCore(requestData.id, undefined, true);
|
223
|
+
|
224
|
+
// Check if the message has already been handled to prevent replay attacks
|
225
|
+
if (core.isAvailable()) {
|
226
|
+
const content = core.getCurrentContent() as RawCoMap;
|
227
|
+
|
228
|
+
if (content.get("$handled") === loadAs.id) {
|
229
|
+
throw new JazzRequestError("Request payload is already handled", 400);
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
// Check if the message is expired as extra protection
|
234
|
+
if (requestData.createdAt + 1000 * 60 < Date.now()) {
|
235
|
+
throw new JazzRequestError("Authentication token is expired", 401);
|
236
|
+
}
|
237
|
+
}
|
238
|
+
|
239
|
+
// Verify the signature of the message to prevent tampering
|
240
|
+
const signPayload = crypto.secureHash({
|
241
|
+
contentPieces: requestData.contentPieces,
|
242
|
+
id: requestData.id,
|
243
|
+
createdAt: requestData.createdAt,
|
244
|
+
signerID: requestData.signerID,
|
245
|
+
});
|
246
|
+
|
247
|
+
if (
|
248
|
+
!safeVerifySignature(
|
249
|
+
crypto,
|
250
|
+
signPayload,
|
251
|
+
requestData.signerID,
|
252
|
+
requestData.authToken,
|
253
|
+
)
|
254
|
+
) {
|
255
|
+
throw new JazzRequestError("Invalid signature", 401);
|
256
|
+
}
|
257
|
+
|
258
|
+
let contentPieces =
|
259
|
+
requestData.contentPieces as CojsonInternalTypes.NewContentMessage[];
|
260
|
+
|
261
|
+
if (type === "request") {
|
262
|
+
const coValueContent = contentPieces.find(
|
263
|
+
(piece) => piece.id === requestData.id,
|
264
|
+
);
|
265
|
+
|
266
|
+
if (coValueContent && coValueContent.header) {
|
267
|
+
const validValues = cojsonInternals.getDependedOnCoValues(
|
268
|
+
coValueContent.header,
|
269
|
+
coValueContent,
|
270
|
+
);
|
271
|
+
validValues.add(requestData.id);
|
272
|
+
contentPieces = contentPieces.filter((piece) =>
|
273
|
+
validValues.has(piece.id),
|
274
|
+
);
|
275
|
+
} else {
|
276
|
+
contentPieces = [];
|
277
|
+
}
|
278
|
+
}
|
279
|
+
|
280
|
+
importContentPieces(contentPieces, loadAs);
|
281
|
+
|
282
|
+
const coValue = await node.loadCoValueCore(requestData.id);
|
283
|
+
const accountId = getCoValueCreatorAccountId(coValue);
|
284
|
+
|
285
|
+
const madeBy = await Account.load(accountId, {
|
286
|
+
loadAs,
|
287
|
+
});
|
288
|
+
|
289
|
+
if (!madeBy) {
|
290
|
+
throw new JazzRequestError("Creator account not found", 400);
|
291
|
+
}
|
292
|
+
|
293
|
+
const coSchema = anySchemaToCoSchema(schema) as CoValueClass<CoMap>;
|
294
|
+
const value = await loadCoValue<CoMap, true>(coSchema, requestData.id, {
|
295
|
+
resolve,
|
296
|
+
loadAs,
|
297
|
+
});
|
298
|
+
|
299
|
+
if (!value) {
|
300
|
+
throw new JazzRequestError("Value not found", 400);
|
301
|
+
}
|
302
|
+
|
303
|
+
if (type === "request") {
|
304
|
+
value._raw.set("$handled", loadAs.id);
|
305
|
+
}
|
306
|
+
|
307
|
+
return {
|
308
|
+
value: value as unknown,
|
309
|
+
madeBy,
|
310
|
+
};
|
311
|
+
}
|
312
|
+
|
313
|
+
function parseSchemaAndResolve<
|
314
|
+
S extends MessageShape,
|
315
|
+
R extends ResolveQuery<CoMapSchema<S>>,
|
316
|
+
>(options: RequestSchemaDefinition<S, R>) {
|
317
|
+
if ("schema" in options) {
|
318
|
+
return {
|
319
|
+
// Using a type cast to reduce the type complexity
|
320
|
+
schema: coMapDefiner(options.schema) as AnyCoMapSchema,
|
321
|
+
resolve: options.resolve as any,
|
322
|
+
};
|
323
|
+
}
|
324
|
+
|
325
|
+
return {
|
326
|
+
schema: coMapDefiner(options) as AnyCoMapSchema,
|
327
|
+
resolve: true as any,
|
328
|
+
};
|
329
|
+
}
|
330
|
+
|
331
|
+
class HttpRoute<
|
332
|
+
RequestShape extends MessageShape = z.core.$ZodLooseShape,
|
333
|
+
RequestResolve extends ResolveQuery<CoMapSchema<RequestShape>> = any,
|
334
|
+
ResponseShape extends MessageShape = z.core.$ZodLooseShape,
|
335
|
+
ResponseResolve extends ResolveQuery<CoMapSchema<ResponseShape>> = any,
|
336
|
+
> {
|
337
|
+
private requestDefinition: {
|
338
|
+
schema: AnyCoMapSchema;
|
339
|
+
resolve: any;
|
340
|
+
};
|
341
|
+
private responseDefinition: {
|
342
|
+
schema: AnyCoMapSchema;
|
343
|
+
resolve: any;
|
344
|
+
};
|
345
|
+
private url: string;
|
346
|
+
private workerId: string;
|
347
|
+
|
348
|
+
constructor(
|
349
|
+
params: RequestOptions<
|
350
|
+
RequestShape,
|
351
|
+
RequestResolve,
|
352
|
+
ResponseShape,
|
353
|
+
ResponseResolve
|
354
|
+
>,
|
355
|
+
) {
|
356
|
+
this.requestDefinition = parseSchemaAndResolve(params.request);
|
357
|
+
this.responseDefinition = parseSchemaAndResolve(params.response);
|
358
|
+
this.url = params.url;
|
359
|
+
this.workerId = params.workerId;
|
360
|
+
|
361
|
+
if (params.workerId === undefined) {
|
362
|
+
throw new TypeError("Worker ID is required");
|
363
|
+
}
|
364
|
+
}
|
365
|
+
|
366
|
+
async send(
|
367
|
+
values: MessageValuePayload<RequestShape>,
|
368
|
+
options?: { owner?: Account },
|
369
|
+
): Promise<Loaded<CoMapSchema<ResponseShape>, ResponseResolve>> {
|
370
|
+
const as = options?.owner ?? Account.getMe();
|
371
|
+
|
372
|
+
const target = await loadWorkerAccountOrGroup(this.workerId, as);
|
373
|
+
if (!target) {
|
374
|
+
throw new JazzRequestError("Worker account not found", 400);
|
375
|
+
}
|
376
|
+
|
377
|
+
const response = await fetch(this.url, {
|
378
|
+
method: "POST",
|
379
|
+
headers: {
|
380
|
+
"Content-Type": "application/json",
|
381
|
+
},
|
382
|
+
body: JSON.stringify(
|
383
|
+
await serializeMessagePayload({
|
384
|
+
type: "request",
|
385
|
+
schema: this.requestDefinition.schema,
|
386
|
+
resolve: true, // export only the envelope
|
387
|
+
value: values,
|
388
|
+
owner: as,
|
389
|
+
target,
|
390
|
+
}),
|
391
|
+
),
|
392
|
+
});
|
393
|
+
|
394
|
+
if (!response.ok) {
|
395
|
+
if (response.headers.has("X-Jazz-Request-Error")) {
|
396
|
+
const error = await response.json();
|
397
|
+
throw new JazzRequestError(error.message, error.code, error.details);
|
398
|
+
}
|
399
|
+
|
400
|
+
throw new JazzRequestError("Request failed", response.status);
|
401
|
+
}
|
402
|
+
|
403
|
+
const responseBody = await response.json();
|
404
|
+
|
405
|
+
const responseParsed = z
|
406
|
+
.object({
|
407
|
+
type: z.literal("success"),
|
408
|
+
payload: z.any(),
|
409
|
+
})
|
410
|
+
.safeParse(responseBody);
|
411
|
+
|
412
|
+
if (!responseParsed.success) {
|
413
|
+
throw new JazzRequestError(
|
414
|
+
"Response payload is not valid",
|
415
|
+
400,
|
416
|
+
responseParsed.error,
|
417
|
+
);
|
418
|
+
}
|
419
|
+
|
420
|
+
const data = await handleMessagePayload({
|
421
|
+
type: "response",
|
422
|
+
schema: this.responseDefinition.schema,
|
423
|
+
resolve: this.responseDefinition.resolve,
|
424
|
+
request: responseParsed.data.payload,
|
425
|
+
loadAs: as,
|
426
|
+
});
|
427
|
+
|
428
|
+
return data.value as Loaded<CoMapSchema<ResponseShape>, ResponseResolve>;
|
429
|
+
}
|
430
|
+
|
431
|
+
handle = async (
|
432
|
+
request: Request,
|
433
|
+
as: Account,
|
434
|
+
callback: (
|
435
|
+
value: Loaded<CoMapSchema<RequestShape>, RequestResolve>,
|
436
|
+
madeBy: Account,
|
437
|
+
) =>
|
438
|
+
| Promise<MessageValuePayload<ResponseShape>>
|
439
|
+
| MessageValuePayload<ResponseShape>,
|
440
|
+
): Promise<Response> => {
|
441
|
+
try {
|
442
|
+
const response = await this.executeHandleRequest(request, as, callback);
|
443
|
+
return response;
|
444
|
+
} catch (error) {
|
445
|
+
// Serialize the error to make it possible to handle it on the client side
|
446
|
+
if (isJazzRequestError(error)) {
|
447
|
+
return new Response(JSON.stringify(error.toJSON()), {
|
448
|
+
status: error.code,
|
449
|
+
headers: {
|
450
|
+
"Content-Type": "application/json",
|
451
|
+
"X-Jazz-Request-Error": "true",
|
452
|
+
},
|
453
|
+
});
|
454
|
+
}
|
455
|
+
|
456
|
+
throw error;
|
457
|
+
}
|
458
|
+
};
|
459
|
+
|
460
|
+
executeHandleRequest = async (
|
461
|
+
request: Request,
|
462
|
+
as: Account,
|
463
|
+
callback: (
|
464
|
+
value: Loaded<CoMapSchema<RequestShape>, RequestResolve>,
|
465
|
+
madeBy: Account,
|
466
|
+
) =>
|
467
|
+
| Promise<MessageValuePayload<ResponseShape>>
|
468
|
+
| MessageValuePayload<ResponseShape>,
|
469
|
+
): Promise<Response> => {
|
470
|
+
const node = as._raw.core.node;
|
471
|
+
const body = await request.json();
|
472
|
+
const data = await handleMessagePayload({
|
473
|
+
type: "request",
|
474
|
+
schema: this.requestDefinition.schema,
|
475
|
+
resolve: this.requestDefinition.resolve,
|
476
|
+
request: body,
|
477
|
+
loadAs: as,
|
478
|
+
});
|
479
|
+
|
480
|
+
const tracking = node.syncManager.trackDirtyCoValues();
|
481
|
+
|
482
|
+
const responseValue = await callback(
|
483
|
+
data.value as Loaded<CoMapSchema<RequestShape>, RequestResolve>,
|
484
|
+
data.madeBy,
|
485
|
+
);
|
486
|
+
|
487
|
+
const responsePayload = await serializeMessagePayload({
|
488
|
+
type: "response",
|
489
|
+
schema: this.responseDefinition.schema,
|
490
|
+
resolve: this.responseDefinition.resolve,
|
491
|
+
value: responseValue,
|
492
|
+
owner: as,
|
493
|
+
target: data.madeBy,
|
494
|
+
});
|
495
|
+
|
496
|
+
const responseBody = JSON.stringify({
|
497
|
+
type: "success",
|
498
|
+
payload: responsePayload,
|
499
|
+
});
|
500
|
+
|
501
|
+
// TODO: Detect the defer support from the environment
|
502
|
+
await Promise.all(
|
503
|
+
Array.from(tracking.done(), (id) => node.syncManager.waitForSync(id)),
|
504
|
+
);
|
505
|
+
|
506
|
+
return new Response(responseBody, {
|
507
|
+
status: 200,
|
508
|
+
headers: {
|
509
|
+
"Content-Type": "application/json",
|
510
|
+
},
|
511
|
+
});
|
512
|
+
};
|
513
|
+
|
514
|
+
get requestSchema(): CoMapSchema<RequestShape> {
|
515
|
+
return this.requestDefinition.schema as CoMapSchema<RequestShape>;
|
516
|
+
}
|
517
|
+
|
518
|
+
get responseSchema() {
|
519
|
+
return this.responseDefinition.schema as CoMapSchema<ResponseShape>;
|
520
|
+
}
|
521
|
+
}
|
522
|
+
|
523
|
+
/**
|
524
|
+
* Define a request route.
|
525
|
+
*
|
526
|
+
* @param params - The parameters for the request route.
|
527
|
+
* @returns The request route.
|
528
|
+
*
|
529
|
+
* @see {@link https://jazz.tools/docs/react/server-side/http-requests}
|
530
|
+
*/
|
531
|
+
export function experimental_defineRequest<
|
532
|
+
RequestShape extends MessageShape,
|
533
|
+
RequestResolve extends ResolveQuery<CoMapSchema<RequestShape>>,
|
534
|
+
ResponseShape extends MessageShape,
|
535
|
+
ResponseResolve extends ResolveQuery<CoMapSchema<ResponseShape>>,
|
536
|
+
>(
|
537
|
+
params: RequestOptions<
|
538
|
+
RequestShape,
|
539
|
+
RequestResolve,
|
540
|
+
ResponseShape,
|
541
|
+
ResponseResolve
|
542
|
+
>,
|
543
|
+
) {
|
544
|
+
return new HttpRoute(params);
|
545
|
+
}
|
546
|
+
|
547
|
+
function getCoValueCreatorAccountId(coValue: CoValueCore) {
|
548
|
+
if (!coValue.isAvailable()) {
|
549
|
+
throw new Error("Unable to load the request payload");
|
550
|
+
}
|
551
|
+
|
552
|
+
const creatorSessionId = coValue.getValidSortedTransactions().at(0)
|
553
|
+
?.txID.sessionID;
|
554
|
+
|
555
|
+
if (!creatorSessionId) {
|
556
|
+
throw new JazzRequestError(
|
557
|
+
"Request payload is not valid, creator session ID not found",
|
558
|
+
400,
|
559
|
+
);
|
560
|
+
}
|
561
|
+
|
562
|
+
const accountId =
|
563
|
+
cojsonInternals.accountOrAgentIDfromSessionID(creatorSessionId);
|
564
|
+
|
565
|
+
if (!isCoValueId(accountId)) {
|
566
|
+
throw new JazzRequestError(
|
567
|
+
"Request payload is not valid, the creator is not a valid account",
|
568
|
+
400,
|
569
|
+
);
|
570
|
+
}
|
571
|
+
|
572
|
+
return accountId;
|
573
|
+
}
|
574
|
+
|
575
|
+
export class JazzRequestError {
|
576
|
+
public readonly isJazzRequestError = true;
|
577
|
+
|
578
|
+
constructor(
|
579
|
+
public readonly message: string,
|
580
|
+
public readonly code: number,
|
581
|
+
public readonly details?: unknown,
|
582
|
+
) {}
|
583
|
+
|
584
|
+
toJSON() {
|
585
|
+
return { message: this.message, code: this.code, details: this.details };
|
586
|
+
}
|
587
|
+
}
|
588
|
+
|
589
|
+
export function isJazzRequestError(error: unknown): error is JazzRequestError {
|
590
|
+
return (
|
591
|
+
error instanceof JazzRequestError ||
|
592
|
+
(typeof error === "object" &&
|
593
|
+
error !== null &&
|
594
|
+
"isJazzRequestError" in error &&
|
595
|
+
Boolean(error.isJazzRequestError))
|
596
|
+
);
|
597
|
+
}
|
598
|
+
|
599
|
+
function safeVerifySignature(
|
600
|
+
crypto: CryptoProvider,
|
601
|
+
signPayload: `hash_z${string}`,
|
602
|
+
signerID: `signer_z${string}`,
|
603
|
+
authToken: `signature_z${string}`,
|
604
|
+
) {
|
605
|
+
try {
|
606
|
+
return crypto.verify(authToken, signPayload, signerID);
|
607
|
+
} catch (error) {
|
608
|
+
return false;
|
609
|
+
}
|
610
|
+
}
|
611
|
+
|
612
|
+
async function loadWorkerAccountOrGroup(id: string, loadAs: Account) {
|
613
|
+
const node = loadAs._raw.core.node;
|
614
|
+
const coValue = await node.loadCoValueCore(id as `co_z${string}`);
|
615
|
+
|
616
|
+
if (!coValue.isAvailable()) {
|
617
|
+
return null;
|
618
|
+
}
|
619
|
+
|
620
|
+
const content = coValue.getCurrentContent();
|
621
|
+
|
622
|
+
if (content instanceof RawAccount) {
|
623
|
+
return Account.load(content.id, {
|
624
|
+
loadAs,
|
625
|
+
});
|
626
|
+
}
|
627
|
+
|
628
|
+
return Group.load(content.id, {
|
629
|
+
loadAs,
|
630
|
+
});
|
631
|
+
}
|
package/src/tools/exports.ts
CHANGED
@@ -54,6 +54,8 @@ export {
|
|
54
54
|
subscribeToCoValue,
|
55
55
|
ImageDefinition,
|
56
56
|
SubscriptionScope,
|
57
|
+
exportCoValue,
|
58
|
+
importContentPieces,
|
57
59
|
} from "./internal.js";
|
58
60
|
|
59
61
|
export {
|
@@ -118,3 +120,9 @@ export {
|
|
118
120
|
type CoRecordSchema,
|
119
121
|
type CoProfileSchema,
|
120
122
|
} from "./internal.js";
|
123
|
+
|
124
|
+
export {
|
125
|
+
experimental_defineRequest,
|
126
|
+
JazzRequestError,
|
127
|
+
isJazzRequestError,
|
128
|
+
} from "./coValues/request.js";
|
@@ -44,7 +44,8 @@ export class SubscriptionScope<D extends CoValue> {
|
|
44
44
|
resolve: RefsToResolve<D>,
|
45
45
|
public id: ID<D>,
|
46
46
|
public schema: RefEncoded<D>,
|
47
|
-
public skipRetry
|
47
|
+
public skipRetry = false,
|
48
|
+
public bestEffortResolution = false,
|
48
49
|
) {
|
49
50
|
this.resolve = resolve;
|
50
51
|
this.value = { type: "unloaded", id };
|
@@ -174,6 +175,10 @@ export class SubscriptionScope<D extends CoValue> {
|
|
174
175
|
return undefined;
|
175
176
|
}
|
176
177
|
|
178
|
+
if (this.bestEffortResolution) {
|
179
|
+
return undefined;
|
180
|
+
}
|
181
|
+
|
177
182
|
for (const [key, value] of this.childErrors.entries()) {
|
178
183
|
// We don't want to block updates if the error is on an autoloaded value
|
179
184
|
if (this.autoloaded.has(key)) {
|
@@ -395,6 +400,8 @@ export class SubscriptionScope<D extends CoValue> {
|
|
395
400
|
true,
|
396
401
|
id as ID<any>,
|
397
402
|
descriptor,
|
403
|
+
this.skipRetry,
|
404
|
+
this.bestEffortResolution,
|
398
405
|
);
|
399
406
|
this.childNodes.set(id, child);
|
400
407
|
child.setListener((value) => this.handleChildUpdate(id, value));
|
@@ -630,6 +637,8 @@ export class SubscriptionScope<D extends CoValue> {
|
|
630
637
|
resolve,
|
631
638
|
id as ID<any>,
|
632
639
|
descriptor,
|
640
|
+
this.skipRetry,
|
641
|
+
this.bestEffortResolution,
|
633
642
|
);
|
634
643
|
this.childNodes.set(id, child);
|
635
644
|
child.setListener((value) => this.handleChildUpdate(id, value, key));
|