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.
Files changed (68) hide show
  1. package/.turbo/turbo-build.log +61 -61
  2. package/CHANGELOG.md +24 -0
  3. package/dist/{chunk-ZIAN4UY5.js → chunk-YOL3XDDW.js} +158 -120
  4. package/dist/chunk-YOL3XDDW.js.map +1 -0
  5. package/dist/index.js +97 -5
  6. package/dist/index.js.map +1 -1
  7. package/dist/media/{chunk-W3S526L3.js → chunk-K6GCHLQU.js} +1 -1
  8. package/dist/media/chunk-K6GCHLQU.js.map +1 -0
  9. package/dist/media/create-image/browser.d.ts +1 -1
  10. package/dist/media/create-image/react-native.d.ts +1 -1
  11. package/dist/media/create-image/react-native.d.ts.map +1 -1
  12. package/dist/media/create-image/server.d.ts +1 -1
  13. package/dist/media/create-image-factory.d.ts +5 -2
  14. package/dist/media/create-image-factory.d.ts.map +1 -1
  15. package/dist/media/index.browser.js +1 -1
  16. package/dist/media/index.d.ts +3 -4
  17. package/dist/media/index.d.ts.map +1 -1
  18. package/dist/media/index.js +1 -1
  19. package/dist/media/index.native.js +63 -28
  20. package/dist/media/index.native.js.map +1 -1
  21. package/dist/media/index.server.js +1 -1
  22. package/dist/react/index.js.map +1 -1
  23. package/dist/react-core/hooks.d.ts +4 -0
  24. package/dist/react-core/hooks.d.ts.map +1 -1
  25. package/dist/react-core/index.js +5 -0
  26. package/dist/react-core/index.js.map +1 -1
  27. package/dist/testing.js +1 -1
  28. package/dist/tools/coValues/coList.d.ts +11 -3
  29. package/dist/tools/coValues/coList.d.ts.map +1 -1
  30. package/dist/tools/coValues/coMap.d.ts +21 -5
  31. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  32. package/dist/tools/coValues/group.d.ts +2 -2
  33. package/dist/tools/coValues/group.d.ts.map +1 -1
  34. package/dist/tools/coValues/inbox.d.ts.map +1 -1
  35. package/dist/tools/coValues/interfaces.d.ts +9 -0
  36. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  37. package/dist/tools/coValues/request.d.ts +70 -0
  38. package/dist/tools/coValues/request.d.ts.map +1 -1
  39. package/dist/tools/exports.d.ts +2 -2
  40. package/dist/tools/exports.d.ts.map +1 -1
  41. package/dist/tools/tests/authenticate-request.test.d.ts +2 -0
  42. package/dist/tools/tests/authenticate-request.test.d.ts.map +1 -0
  43. package/dist/tools/tests/coList.unique.test.d.ts +2 -0
  44. package/dist/tools/tests/coList.unique.test.d.ts.map +1 -0
  45. package/dist/tools/tests/coMap.unique.test.d.ts +2 -0
  46. package/dist/tools/tests/coMap.unique.test.d.ts.map +1 -0
  47. package/package.json +8 -4
  48. package/src/media/create-image/react-native.ts +75 -30
  49. package/src/media/create-image-factory.test.ts +18 -0
  50. package/src/media/create-image-factory.ts +6 -1
  51. package/src/media/index.ts +7 -4
  52. package/src/react-core/hooks.ts +8 -0
  53. package/src/react-core/tests/useAccount.test.ts +61 -1
  54. package/src/react-core/tests/usePassPhraseAuth.test.ts +74 -2
  55. package/src/tools/coValues/coList.ts +38 -35
  56. package/src/tools/coValues/coMap.ts +38 -38
  57. package/src/tools/coValues/group.ts +5 -1
  58. package/src/tools/coValues/inbox.ts +4 -3
  59. package/src/tools/coValues/interfaces.ts +88 -0
  60. package/src/tools/coValues/request.ts +188 -4
  61. package/src/tools/exports.ts +4 -0
  62. package/src/tools/tests/authenticate-request.test.ts +194 -0
  63. package/src/tools/tests/coList.test.ts +0 -190
  64. package/src/tools/tests/coList.unique.test.ts +244 -0
  65. package/src/tools/tests/coMap.test.ts +0 -433
  66. package/src/tools/tests/coMap.unique.test.ts +474 -0
  67. package/dist/chunk-ZIAN4UY5.js.map +0 -1
  68. 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
- return CoMap._findUnique(unique, ownerID, as);
439
+ const header = CoMap._getUniqueHeader(unique, ownerID);
440
+
441
+ return getIdFromHeader(header, as);
438
442
  }
439
443
 
440
444
  /** @internal */
441
- static _findUnique<M extends CoMap>(
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
- as ||= activeAccountContext.get();
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 mapId = CoMap._findUnique(
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 await loadCoValueWithoutMe(this, mapId, {
523
- ...options,
524
- loadAs: options.owner.$jazz.loadedAs,
525
- skipRetry: true,
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<M extends CoMap, const R extends RefsToResolve<M> = true>(
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
- return loadCoValueWithoutMe(
548
- this,
549
- CoMap._findUnique(unique, ownerID, options?.loadAs),
550
- { ...options, skipRetry: true },
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?: { resolve?: RefsToResolveStrict<G, R>; loadAs?: Account },
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.txID.sessionID);
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.txID.sessionID,
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 = `${tx.txID.sessionID}/${tx.txID.txIndex}` as const;
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: z.custom<`co_z${string}`>(isCoValueId),
182
+ id: coIdSchema,
178
183
  createdAt: z.number(),
179
- authToken: z.custom<`signature_z${string}`>(
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
+ }
@@ -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
+ });