jazz-tools 0.18.26 → 0.18.28
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 +61 -61
- package/CHANGELOG.md +24 -0
- package/dist/{chunk-ZIAN4UY5.js → chunk-YOL3XDDW.js} +158 -120
- package/dist/chunk-YOL3XDDW.js.map +1 -0
- package/dist/index.js +97 -5
- package/dist/index.js.map +1 -1
- package/dist/media/{chunk-W3S526L3.js → chunk-K6GCHLQU.js} +1 -1
- package/dist/media/chunk-K6GCHLQU.js.map +1 -0
- package/dist/media/create-image/browser.d.ts +1 -1
- package/dist/media/create-image/react-native.d.ts +1 -1
- package/dist/media/create-image/react-native.d.ts.map +1 -1
- package/dist/media/create-image/server.d.ts +1 -1
- package/dist/media/create-image-factory.d.ts +5 -2
- package/dist/media/create-image-factory.d.ts.map +1 -1
- package/dist/media/index.browser.js +1 -1
- package/dist/media/index.d.ts +3 -4
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/index.native.js +63 -28
- package/dist/media/index.native.js.map +1 -1
- package/dist/media/index.server.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +4 -0
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +5 -0
- package/dist/react-core/index.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/coList.d.ts +11 -3
- package/dist/tools/coValues/coList.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts +21 -5
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/coValues/group.d.ts +2 -2
- 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 +9 -0
- package/dist/tools/coValues/interfaces.d.ts.map +1 -1
- package/dist/tools/coValues/request.d.ts +70 -0
- package/dist/tools/coValues/request.d.ts.map +1 -1
- package/dist/tools/exports.d.ts +2 -2
- package/dist/tools/exports.d.ts.map +1 -1
- package/dist/tools/tests/authenticate-request.test.d.ts +2 -0
- package/dist/tools/tests/authenticate-request.test.d.ts.map +1 -0
- package/dist/tools/tests/coList.unique.test.d.ts +2 -0
- package/dist/tools/tests/coList.unique.test.d.ts.map +1 -0
- package/dist/tools/tests/coMap.unique.test.d.ts +2 -0
- package/dist/tools/tests/coMap.unique.test.d.ts.map +1 -0
- package/package.json +8 -4
- package/src/media/create-image/react-native.ts +75 -30
- package/src/media/create-image-factory.test.ts +18 -0
- package/src/media/create-image-factory.ts +6 -1
- package/src/media/index.ts +7 -4
- package/src/react-core/hooks.ts +8 -0
- package/src/react-core/tests/useAccount.test.ts +61 -1
- package/src/react-core/tests/usePassPhraseAuth.test.ts +74 -2
- package/src/tools/coValues/coList.ts +38 -35
- package/src/tools/coValues/coMap.ts +38 -38
- package/src/tools/coValues/group.ts +5 -1
- package/src/tools/coValues/inbox.ts +4 -3
- package/src/tools/coValues/interfaces.ts +88 -0
- package/src/tools/coValues/request.ts +188 -4
- package/src/tools/exports.ts +4 -0
- package/src/tools/tests/authenticate-request.test.ts +194 -0
- package/src/tools/tests/coList.test.ts +0 -190
- package/src/tools/tests/coList.unique.test.ts +244 -0
- package/src/tools/tests/coMap.test.ts +0 -433
- package/src/tools/tests/coMap.unique.test.ts +474 -0
- package/dist/chunk-ZIAN4UY5.js.map +0 -1
- package/dist/media/chunk-W3S526L3.js.map +0 -1
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
SubscribeRestArgs,
|
|
29
29
|
TypeSym,
|
|
30
30
|
BranchDefinition,
|
|
31
|
+
getIdFromHeader,
|
|
32
|
+
unstable_loadUnique,
|
|
31
33
|
} from "../internal.js";
|
|
32
34
|
import {
|
|
33
35
|
Account,
|
|
@@ -434,19 +436,17 @@ export class CoMap extends CoValueBase implements CoValue {
|
|
|
434
436
|
ownerID: ID<Account> | ID<Group>,
|
|
435
437
|
as?: Account | Group | AnonymousJazzAgent,
|
|
436
438
|
) {
|
|
437
|
-
|
|
439
|
+
const header = CoMap._getUniqueHeader(unique, ownerID);
|
|
440
|
+
|
|
441
|
+
return getIdFromHeader(header, as);
|
|
438
442
|
}
|
|
439
443
|
|
|
440
444
|
/** @internal */
|
|
441
|
-
static
|
|
442
|
-
this: CoValueClass<M>,
|
|
445
|
+
static _getUniqueHeader(
|
|
443
446
|
unique: CoValueUniqueness["uniqueness"],
|
|
444
447
|
ownerID: ID<Account> | ID<Group>,
|
|
445
|
-
as?: Account | Group | AnonymousJazzAgent,
|
|
446
448
|
) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const header = {
|
|
449
|
+
return {
|
|
450
450
|
type: "comap" as const,
|
|
451
451
|
ruleset: {
|
|
452
452
|
type: "ownedByGroup" as const,
|
|
@@ -455,9 +455,6 @@ export class CoMap extends CoValueBase implements CoValue {
|
|
|
455
455
|
meta: null,
|
|
456
456
|
uniqueness: unique,
|
|
457
457
|
};
|
|
458
|
-
const crypto =
|
|
459
|
-
as[TypeSym] === "Anonymous" ? as.node.crypto : as.$jazz.localNode.crypto;
|
|
460
|
-
return cojsonInternals.idforHeader(header, crypto) as ID<M>;
|
|
461
458
|
}
|
|
462
459
|
|
|
463
460
|
/**
|
|
@@ -497,32 +494,24 @@ export class CoMap extends CoValueBase implements CoValue {
|
|
|
497
494
|
resolve?: RefsToResolveStrict<M, R>;
|
|
498
495
|
},
|
|
499
496
|
): Promise<Resolved<M, R> | null> {
|
|
500
|
-
const
|
|
497
|
+
const header = CoMap._getUniqueHeader(
|
|
501
498
|
options.unique,
|
|
502
499
|
options.owner.$jazz.id,
|
|
503
|
-
options.owner.$jazz.loadedAs,
|
|
504
500
|
);
|
|
505
|
-
let map: Resolved<M, R> | null = await loadCoValueWithoutMe(this, mapId, {
|
|
506
|
-
...options,
|
|
507
|
-
loadAs: options.owner.$jazz.loadedAs,
|
|
508
|
-
skipRetry: true,
|
|
509
|
-
});
|
|
510
|
-
if (!map) {
|
|
511
|
-
const instance = new this();
|
|
512
|
-
map = CoMap._createCoMap(instance, options.value, {
|
|
513
|
-
owner: options.owner,
|
|
514
|
-
unique: options.unique,
|
|
515
|
-
}) as Resolved<M, R>;
|
|
516
|
-
} else {
|
|
517
|
-
(map as M).$jazz.applyDiff(
|
|
518
|
-
options.value as unknown as Partial<CoMapInit<M>>,
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
501
|
|
|
522
|
-
return
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
502
|
+
return unstable_loadUnique(this, {
|
|
503
|
+
header,
|
|
504
|
+
owner: options.owner,
|
|
505
|
+
resolve: options.resolve,
|
|
506
|
+
onCreateWhenMissing: () => {
|
|
507
|
+
(this as any).create(options.value, {
|
|
508
|
+
owner: options.owner,
|
|
509
|
+
unique: options.unique,
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
onUpdateWhenFound(value) {
|
|
513
|
+
value.$jazz.applyDiff(options.value);
|
|
514
|
+
},
|
|
526
515
|
});
|
|
527
516
|
}
|
|
528
517
|
|
|
@@ -535,7 +524,10 @@ export class CoMap extends CoValueBase implements CoValue {
|
|
|
535
524
|
*
|
|
536
525
|
* @deprecated Use `co.map(...).loadUnique` instead.
|
|
537
526
|
*/
|
|
538
|
-
static loadUnique<
|
|
527
|
+
static async loadUnique<
|
|
528
|
+
M extends CoMap,
|
|
529
|
+
const R extends RefsToResolve<M> = true,
|
|
530
|
+
>(
|
|
539
531
|
this: CoValueClass<M>,
|
|
540
532
|
unique: CoValueUniqueness["uniqueness"],
|
|
541
533
|
ownerID: ID<Account> | ID<Group>,
|
|
@@ -544,11 +536,19 @@ export class CoMap extends CoValueBase implements CoValue {
|
|
|
544
536
|
loadAs?: Account | AnonymousJazzAgent;
|
|
545
537
|
},
|
|
546
538
|
): Promise<Resolved<M, R> | null> {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
);
|
|
539
|
+
const header = CoMap._getUniqueHeader(unique, ownerID);
|
|
540
|
+
|
|
541
|
+
const owner = await Group.load(ownerID, {
|
|
542
|
+
loadAs: options?.loadAs,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
if (!owner) return owner;
|
|
546
|
+
|
|
547
|
+
return unstable_loadUnique(this, {
|
|
548
|
+
header,
|
|
549
|
+
owner,
|
|
550
|
+
resolve: options?.resolve,
|
|
551
|
+
});
|
|
552
552
|
}
|
|
553
553
|
}
|
|
554
554
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type Role,
|
|
10
10
|
} from "cojson";
|
|
11
11
|
import {
|
|
12
|
+
AnonymousJazzAgent,
|
|
12
13
|
BranchDefinition,
|
|
13
14
|
CoValue,
|
|
14
15
|
CoValueClass,
|
|
@@ -272,7 +273,10 @@ export class Group extends CoValueBase implements CoValue {
|
|
|
272
273
|
static load<G extends Group, const R extends RefsToResolve<G>>(
|
|
273
274
|
this: CoValueClass<G>,
|
|
274
275
|
id: ID<G>,
|
|
275
|
-
options?: {
|
|
276
|
+
options?: {
|
|
277
|
+
resolve?: RefsToResolveStrict<G, R>;
|
|
278
|
+
loadAs?: Account | AnonymousJazzAgent;
|
|
279
|
+
},
|
|
276
280
|
): Promise<Resolved<G, R> | null> {
|
|
277
281
|
return loadCoValueWithoutMe(this, id, options);
|
|
278
282
|
}
|
|
@@ -293,12 +293,12 @@ export class Inbox {
|
|
|
293
293
|
|
|
294
294
|
const handleNewMessages = () => {
|
|
295
295
|
for (const tx of messagesFeed.getNewItems()) {
|
|
296
|
-
const accountID = getAccountIDfromSessionID(tx.
|
|
296
|
+
const accountID = getAccountIDfromSessionID(tx.currentTxID.sessionID);
|
|
297
297
|
|
|
298
298
|
if (!accountID) {
|
|
299
299
|
console.warn(
|
|
300
300
|
"Received message from unknown account",
|
|
301
|
-
tx.
|
|
301
|
+
tx.currentTxID.sessionID,
|
|
302
302
|
);
|
|
303
303
|
continue;
|
|
304
304
|
}
|
|
@@ -309,7 +309,8 @@ export class Inbox {
|
|
|
309
309
|
continue;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
const txKey =
|
|
312
|
+
const txKey =
|
|
313
|
+
`${tx.currentTxID.sessionID}/${tx.currentTxID.txIndex}` as const;
|
|
313
314
|
|
|
314
315
|
if (processed.has(txKey)) {
|
|
315
316
|
continue;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
cojsonInternals,
|
|
2
3
|
type CoValueUniqueness,
|
|
3
4
|
type CojsonInternalTypes,
|
|
4
5
|
type RawCoValue,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
inspect,
|
|
26
27
|
} from "../internal.js";
|
|
27
28
|
import type { BranchDefinition } from "../subscribe/types.js";
|
|
29
|
+
import { CoValueHeader } from "cojson/dist/coValueCore/verifiedState.js";
|
|
28
30
|
|
|
29
31
|
/** @category Abstract interfaces */
|
|
30
32
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -446,6 +448,92 @@ export function parseGroupCreateOptions(
|
|
|
446
448
|
: { owner: options.owner ?? activeAccountContext.get() };
|
|
447
449
|
}
|
|
448
450
|
|
|
451
|
+
export function getIdFromHeader(
|
|
452
|
+
header: CoValueHeader,
|
|
453
|
+
loadAs?: Account | AnonymousJazzAgent | Group,
|
|
454
|
+
) {
|
|
455
|
+
loadAs ||= activeAccountContext.get();
|
|
456
|
+
|
|
457
|
+
const node =
|
|
458
|
+
loadAs[TypeSym] === "Anonymous" ? loadAs.node : loadAs.$jazz.localNode;
|
|
459
|
+
|
|
460
|
+
return cojsonInternals.idforHeader(header, node.crypto);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export async function unstable_loadUnique<
|
|
464
|
+
V extends CoValue,
|
|
465
|
+
R extends RefsToResolve<V>,
|
|
466
|
+
>(
|
|
467
|
+
cls: CoValueClass<V>,
|
|
468
|
+
options: {
|
|
469
|
+
header: CoValueHeader;
|
|
470
|
+
onCreateWhenMissing?: () => void;
|
|
471
|
+
onUpdateWhenFound?: (value: Resolved<V, R>) => void;
|
|
472
|
+
owner: Account | Group;
|
|
473
|
+
resolve?: RefsToResolveStrict<V, R>;
|
|
474
|
+
},
|
|
475
|
+
): Promise<Resolved<V, R> | null> {
|
|
476
|
+
const loadAs = options.owner.$jazz.loadedAs;
|
|
477
|
+
|
|
478
|
+
const node =
|
|
479
|
+
loadAs[TypeSym] === "Anonymous" ? loadAs.node : loadAs.$jazz.localNode;
|
|
480
|
+
|
|
481
|
+
const id = cojsonInternals.idforHeader(options.header, node.crypto);
|
|
482
|
+
|
|
483
|
+
// We first try to load the unique value without using resolve and without
|
|
484
|
+
// retrying failures
|
|
485
|
+
// This way when we want to upsert we are sure that, if the load failed
|
|
486
|
+
// it failed because the unique value was missing
|
|
487
|
+
let result = await loadCoValueWithoutMe(cls, id, {
|
|
488
|
+
skipRetry: true,
|
|
489
|
+
loadAs,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (options.onCreateWhenMissing) {
|
|
493
|
+
// if load returns unavailable, we check the state in localNode
|
|
494
|
+
// to ward against race conditions that would happen when
|
|
495
|
+
// running the same upsert unique concurrently
|
|
496
|
+
if (!result && node.getCoValue(id).hasVerifiedContent()) {
|
|
497
|
+
result = await loadCoValueWithoutMe(cls, id, {
|
|
498
|
+
loadAs,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!result) {
|
|
503
|
+
options.onCreateWhenMissing();
|
|
504
|
+
|
|
505
|
+
return loadCoValueWithoutMe(cls, id, {
|
|
506
|
+
loadAs,
|
|
507
|
+
resolve: options.resolve,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!result) return result;
|
|
513
|
+
|
|
514
|
+
if (options.onUpdateWhenFound) {
|
|
515
|
+
// we deeply load the value, retrying any failures
|
|
516
|
+
const loaded = await loadCoValueWithoutMe(cls, id, {
|
|
517
|
+
loadAs,
|
|
518
|
+
resolve: options.resolve,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (loaded) {
|
|
522
|
+
// we don't return the update result because
|
|
523
|
+
// we want to run another load to backfill any possible partially loaded
|
|
524
|
+
// values that have been set in the update
|
|
525
|
+
options.onUpdateWhenFound(loaded);
|
|
526
|
+
} else {
|
|
527
|
+
return loaded;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return loadCoValueWithoutMe(cls, id, {
|
|
532
|
+
loadAs,
|
|
533
|
+
resolve: options.resolve,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
449
537
|
/**
|
|
450
538
|
* Deeply export a CoValue to a content piece.
|
|
451
539
|
*
|
|
@@ -172,13 +172,16 @@ async function serializeMessagePayload({
|
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
const coIdSchema = z.custom<`co_z${string}`>(isCoValueId);
|
|
176
|
+
const signatureSchema = z.custom<`signature_z${string}`>(
|
|
177
|
+
(value) => typeof value === "string" && value.startsWith("signature_z"),
|
|
178
|
+
);
|
|
179
|
+
|
|
175
180
|
const requestSchema = z.object({
|
|
176
181
|
contentPieces: z.array(z.json()),
|
|
177
|
-
id:
|
|
182
|
+
id: coIdSchema,
|
|
178
183
|
createdAt: z.number(),
|
|
179
|
-
authToken:
|
|
180
|
-
(value) => typeof value === "string" && value.startsWith("signature_z"),
|
|
181
|
-
),
|
|
184
|
+
authToken: signatureSchema,
|
|
182
185
|
signerID: z.custom<`signer_z${string}`>(
|
|
183
186
|
(value) => typeof value === "string" && value.startsWith("signer_z"),
|
|
184
187
|
),
|
|
@@ -631,3 +634,184 @@ async function loadWorkerAccountOrGroup(id: string, loadAs: Account) {
|
|
|
631
634
|
loadAs,
|
|
632
635
|
});
|
|
633
636
|
}
|
|
637
|
+
|
|
638
|
+
function defaultGetToken(request: Request) {
|
|
639
|
+
const headerValue = request.headers.get("Authorization");
|
|
640
|
+
if (headerValue?.startsWith("Jazz ")) {
|
|
641
|
+
return headerValue.replace("Jazz ", "");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (headerValue) {
|
|
645
|
+
console.warn(
|
|
646
|
+
"An Authorization header was found, but it did not start with 'Jazz '. If this is intentional, you can specify the location of the token using the `getToken` option.",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Authenticates a Request by verifying a signed authentication token.
|
|
655
|
+
*
|
|
656
|
+
* - If a token is not provided, the returned account is `undefined` and no error is returned.
|
|
657
|
+
* - If a valid token is provided, the signer account is returned.
|
|
658
|
+
* - If an invalid token is provided, an error is returned detailing the validation error, and the returned account is `undefined`.
|
|
659
|
+
*
|
|
660
|
+
* @see {@link generateAuthToken} for generating a token.
|
|
661
|
+
*
|
|
662
|
+
* Note: This function does not perform any authorization checks, it only verifies if - **when provided** - a token is valid. It is up to the caller to perform any additional authorization checks, if needed.
|
|
663
|
+
*
|
|
664
|
+
* @param request - The request to authenticate.
|
|
665
|
+
* @param options - The options for the authentication.
|
|
666
|
+
* @param options.expiration - The expiration time of the token in milliseconds, defaults to 1 minute.
|
|
667
|
+
* @param options.loadAs - The account to load the token from, defaults to the current active account.
|
|
668
|
+
* @param options.getToken - If specified, this function will be used to get the token from the request. By default the token is expected to be in the `Authorization` header in the form of `Jazz <token>`.
|
|
669
|
+
* @returns The account if it is valid, otherwise an error.
|
|
670
|
+
*
|
|
671
|
+
* @example
|
|
672
|
+
* ```ts
|
|
673
|
+
* const { account, error } = await authenticateRequest(request);
|
|
674
|
+
* if (error) {
|
|
675
|
+
* return new Response(JSON.stringify(error), { status: 401 });
|
|
676
|
+
* }
|
|
677
|
+
* ```
|
|
678
|
+
*/
|
|
679
|
+
export async function authenticateRequest(
|
|
680
|
+
request: Request,
|
|
681
|
+
options?: {
|
|
682
|
+
expiration?: number;
|
|
683
|
+
loadAs?: Account;
|
|
684
|
+
getToken?: (request: Request) => string | undefined | null;
|
|
685
|
+
},
|
|
686
|
+
): Promise<
|
|
687
|
+
| {
|
|
688
|
+
account?: Account;
|
|
689
|
+
error?: never;
|
|
690
|
+
}
|
|
691
|
+
| {
|
|
692
|
+
account?: never;
|
|
693
|
+
error: { message: string; details?: unknown };
|
|
694
|
+
}
|
|
695
|
+
> {
|
|
696
|
+
const token = options?.getToken?.(request) ?? defaultGetToken(request);
|
|
697
|
+
|
|
698
|
+
if (!token) {
|
|
699
|
+
return {};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const { account, error } = await parseAuthToken(token, {
|
|
703
|
+
loadAs: options?.loadAs,
|
|
704
|
+
expiration: options?.expiration ?? 1000 * 60,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
if (error) {
|
|
708
|
+
return { error };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return { account, error };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Generates an authentication token for a given account. This token can be used to authenticate a request. See {@link authenticateRequest} for more details.
|
|
716
|
+
*
|
|
717
|
+
* @param as - The account to generate the token for, defaults to the current active account.
|
|
718
|
+
* @returns The authentication token.
|
|
719
|
+
*
|
|
720
|
+
* @example Make a fetch request with the token
|
|
721
|
+
* ```ts
|
|
722
|
+
* const token = generateAuthToken();
|
|
723
|
+
* const response = await fetch(url, {
|
|
724
|
+
* headers: {
|
|
725
|
+
* Authorization: `Jazz ${token}`,
|
|
726
|
+
* },
|
|
727
|
+
* });
|
|
728
|
+
* ```
|
|
729
|
+
*/
|
|
730
|
+
|
|
731
|
+
export function generateAuthToken(as?: Account) {
|
|
732
|
+
const account = as ?? Account.getMe();
|
|
733
|
+
const node = account.$jazz.localNode;
|
|
734
|
+
const crypto = node.crypto;
|
|
735
|
+
|
|
736
|
+
const agent = node.getCurrentAgent();
|
|
737
|
+
const signerSecret = agent.currentSignerSecret();
|
|
738
|
+
|
|
739
|
+
const createdAt = Date.now();
|
|
740
|
+
|
|
741
|
+
const signPayload = crypto.secureHash({
|
|
742
|
+
id: account.$jazz.id,
|
|
743
|
+
createdAt,
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
const authToken = crypto.sign(signerSecret, signPayload);
|
|
747
|
+
|
|
748
|
+
return `${authToken}~${account.$jazz.id}~${createdAt}`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export async function parseAuthToken(
|
|
752
|
+
authToken: string,
|
|
753
|
+
options?: { loadAs?: Account; expiration?: number },
|
|
754
|
+
): Promise<
|
|
755
|
+
| { account: Account; error?: never }
|
|
756
|
+
| { account?: never; error: { message: string; details?: unknown } }
|
|
757
|
+
> {
|
|
758
|
+
const expiration = options?.expiration ?? 1_000 * 60; // 1 minute
|
|
759
|
+
|
|
760
|
+
const parsed = z
|
|
761
|
+
.tuple([signatureSchema, coIdSchema, z.string().transform(Number)])
|
|
762
|
+
.safeParse(authToken.split("~"));
|
|
763
|
+
|
|
764
|
+
if (!parsed.success) {
|
|
765
|
+
return {
|
|
766
|
+
error: {
|
|
767
|
+
message: "Invalid token",
|
|
768
|
+
details: parsed.error,
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const [signature, id, createdAt] = parsed.data;
|
|
774
|
+
|
|
775
|
+
if (createdAt + expiration < Date.now()) {
|
|
776
|
+
return {
|
|
777
|
+
error: {
|
|
778
|
+
message: "Token expired",
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const account = await Account.load(id, { loadAs: options?.loadAs });
|
|
784
|
+
|
|
785
|
+
if (!account) {
|
|
786
|
+
return {
|
|
787
|
+
error: {
|
|
788
|
+
message: "Failed to load account",
|
|
789
|
+
details: { id },
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const node = account.$jazz.localNode;
|
|
795
|
+
const crypto = node.crypto;
|
|
796
|
+
|
|
797
|
+
// Verify the signature of the message to prevent tampering
|
|
798
|
+
const signPayload = crypto.secureHash({
|
|
799
|
+
id: account.$jazz.id,
|
|
800
|
+
createdAt: Number(createdAt),
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const agentID = account.$jazz.raw.currentAgentID();
|
|
804
|
+
const signerID = crypto.getAgentSignerID(agentID);
|
|
805
|
+
|
|
806
|
+
if (!crypto.verify(signature, signPayload, signerID)) {
|
|
807
|
+
return {
|
|
808
|
+
error: {
|
|
809
|
+
message: "Invalid signature",
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
account,
|
|
816
|
+
};
|
|
817
|
+
}
|
package/src/tools/exports.ts
CHANGED
|
@@ -36,6 +36,7 @@ export type {
|
|
|
36
36
|
AccountClass,
|
|
37
37
|
AccountCreationProps,
|
|
38
38
|
BaseProfileShape,
|
|
39
|
+
unstable_loadUnique,
|
|
39
40
|
} from "./internal.js";
|
|
40
41
|
|
|
41
42
|
export {
|
|
@@ -116,6 +117,9 @@ export {
|
|
|
116
117
|
experimental_defineRequest,
|
|
117
118
|
JazzRequestError,
|
|
118
119
|
isJazzRequestError,
|
|
120
|
+
authenticateRequest,
|
|
121
|
+
generateAuthToken,
|
|
122
|
+
parseAuthToken,
|
|
119
123
|
type HttpRoute,
|
|
120
124
|
} from "./coValues/request.js";
|
|
121
125
|
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { authenticateRequest, generateAuthToken } from "../coValues/request.js";
|
|
3
|
+
import { createJazzTestAccount } from "../testing.js";
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("authenticateRequest", () => {
|
|
10
|
+
it("should correctly authenticate a request", async () => {
|
|
11
|
+
const me = await createJazzTestAccount({
|
|
12
|
+
isCurrentActiveAccount: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const token = generateAuthToken();
|
|
16
|
+
|
|
17
|
+
const { account, error } = await authenticateRequest(
|
|
18
|
+
new Request("https://api.example.com/api/user", {
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: `Jazz ${token}`,
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(error).toBeUndefined();
|
|
26
|
+
expect(account?.$jazz.id).toBe(me.$jazz.id);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should not return an account if no token is provided", async () => {
|
|
30
|
+
await createJazzTestAccount({
|
|
31
|
+
isCurrentActiveAccount: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const { account, error } = await authenticateRequest(
|
|
35
|
+
new Request("https://api.example.com/api/user", {}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(error).toBeUndefined();
|
|
39
|
+
expect(account).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should return an error if the token is invalid", async () => {
|
|
43
|
+
const { account, error } = await authenticateRequest(
|
|
44
|
+
new Request("https://api.example.com/api/user", {
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Jazz invalid~invalid~invalid`,
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(error).toMatchObject(
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
message: "Invalid token",
|
|
54
|
+
details: expect.anything(),
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
expect(account).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should return an error if the token is malformed", async () => {
|
|
61
|
+
const { account, error } = await authenticateRequest(
|
|
62
|
+
new Request("https://api.example.com/api/user", {
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Jazz malformed`,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(error).toMatchObject(
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
message: "Invalid token",
|
|
72
|
+
details: expect.anything(),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
expect(account).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should be resilient to tampering", async () => {
|
|
79
|
+
await createJazzTestAccount({
|
|
80
|
+
isCurrentActiveAccount: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const token = generateAuthToken();
|
|
84
|
+
const tokenParts = token.split("~");
|
|
85
|
+
tokenParts[2] = "999999999999999";
|
|
86
|
+
const tamperedToken = tokenParts.join("~");
|
|
87
|
+
|
|
88
|
+
const { account, error } = await authenticateRequest(
|
|
89
|
+
new Request("https://api.example.com/api/user", {
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: `Jazz ${tamperedToken}`,
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(error).toMatchObject(
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
message: "Invalid signature",
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
expect(account).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should return an error if the token is expired", async () => {
|
|
105
|
+
await createJazzTestAccount({
|
|
106
|
+
isCurrentActiveAccount: true,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const token = generateAuthToken();
|
|
110
|
+
|
|
111
|
+
const { account, error } = await authenticateRequest(
|
|
112
|
+
new Request("https://api.example.com/api/user", {
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Jazz ${token}`,
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
{
|
|
118
|
+
expiration: -1000,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(error).toMatchObject(
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
message: "Token expired",
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
expect(account).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should treat the request as unauthenticated if the token is not in the default format, even if present.", async () => {
|
|
131
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
132
|
+
await createJazzTestAccount({
|
|
133
|
+
isCurrentActiveAccount: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const token = generateAuthToken();
|
|
137
|
+
|
|
138
|
+
const { account, error } = await authenticateRequest(
|
|
139
|
+
new Request("https://api.example.com/api/user", {
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `${token}`,
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(console.warn).toHaveBeenCalled();
|
|
147
|
+
expect(account).toBeUndefined();
|
|
148
|
+
expect(error).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should correctly validate a request when the token is in a non standard location", async () => {
|
|
152
|
+
const me = await createJazzTestAccount({
|
|
153
|
+
isCurrentActiveAccount: true,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const token = generateAuthToken();
|
|
157
|
+
|
|
158
|
+
const { account, error } = await authenticateRequest(
|
|
159
|
+
new Request("https://api.example.com/api/user", {
|
|
160
|
+
headers: {
|
|
161
|
+
["x-jazz-auth-token"]: `${token}`,
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
{
|
|
165
|
+
getToken: (request) => request.headers.get("x-jazz-auth-token"),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(error).toBeUndefined();
|
|
170
|
+
expect(account?.$jazz.id).toBe(me.$jazz.id);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should correctly validate a request when the token is generated from a non active account", async () => {
|
|
174
|
+
const notAnActiveAccount = await createJazzTestAccount({
|
|
175
|
+
isCurrentActiveAccount: false,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const token = generateAuthToken(notAnActiveAccount);
|
|
179
|
+
|
|
180
|
+
const { account, error } = await authenticateRequest(
|
|
181
|
+
new Request("https://api.example.com/api/user", {
|
|
182
|
+
headers: {
|
|
183
|
+
Authorization: `Jazz ${token}`,
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
{
|
|
187
|
+
loadAs: notAnActiveAccount,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(error).toBeUndefined();
|
|
192
|
+
expect(account?.$jazz.id).toBe(notAnActiveAccount.$jazz.id);
|
|
193
|
+
});
|
|
194
|
+
});
|