jazz-tools 0.18.5 → 0.18.7

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 (111) hide show
  1. package/.turbo/turbo-build.log +57 -57
  2. package/CHANGELOG.md +33 -0
  3. package/dist/better-auth/auth/client.d.ts.map +1 -1
  4. package/dist/better-auth/auth/client.js +7 -1
  5. package/dist/better-auth/auth/client.js.map +1 -1
  6. package/dist/better-auth/auth/react.d.ts +0 -2145
  7. package/dist/better-auth/auth/react.d.ts.map +1 -1
  8. package/dist/better-auth/auth/react.js +2 -14
  9. package/dist/better-auth/auth/react.js.map +1 -1
  10. package/dist/better-auth/auth/server.d.ts.map +1 -1
  11. package/dist/better-auth/auth/server.js +77 -22
  12. package/dist/better-auth/auth/server.js.map +1 -1
  13. package/dist/better-auth/auth/tests/react.test.d.ts +2 -0
  14. package/dist/better-auth/auth/tests/react.test.d.ts.map +1 -0
  15. package/dist/{chunk-3LE7N6TH.js → chunk-CFAY3FMQ.js} +192 -101
  16. package/dist/chunk-CFAY3FMQ.js.map +1 -0
  17. package/dist/index.js +1 -1
  18. package/dist/inspector/{custom-element-WCY6D3QJ.js → custom-element-G6SPZEBR.js} +308 -97
  19. package/dist/inspector/custom-element-G6SPZEBR.js.map +1 -0
  20. package/dist/inspector/index.d.ts +5 -1
  21. package/dist/inspector/index.d.ts.map +1 -1
  22. package/dist/inspector/index.js +318 -56
  23. package/dist/inspector/index.js.map +1 -1
  24. package/dist/inspector/register-custom-element.js +1 -1
  25. package/dist/inspector/ui/button.d.ts +1 -1
  26. package/dist/inspector/ui/button.d.ts.map +1 -1
  27. package/dist/inspector/ui/heading.d.ts +2 -1
  28. package/dist/inspector/ui/heading.d.ts.map +1 -1
  29. package/dist/inspector/ui/input.d.ts.map +1 -1
  30. package/dist/inspector/ui/modal.d.ts +16 -0
  31. package/dist/inspector/ui/modal.d.ts.map +1 -0
  32. package/dist/inspector/viewer/delete-local-data.d.ts +2 -0
  33. package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -0
  34. package/dist/inspector/viewer/{inpsector-button.d.ts → inspector-button.d.ts} +1 -1
  35. package/dist/inspector/viewer/{inpsector-button.d.ts.map → inspector-button.d.ts.map} +1 -1
  36. package/dist/inspector/viewer/new-app.d.ts +1 -4
  37. package/dist/inspector/viewer/new-app.d.ts.map +1 -1
  38. package/dist/react/hooks.d.ts +1 -1
  39. package/dist/react/hooks.d.ts.map +1 -1
  40. package/dist/react/index.d.ts +1 -1
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +3 -1
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react-core/hooks.d.ts +133 -0
  45. package/dist/react-core/hooks.d.ts.map +1 -1
  46. package/dist/react-core/index.js +83 -17
  47. package/dist/react-core/index.js.map +1 -1
  48. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts +2 -0
  49. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts.map +1 -0
  50. package/dist/react-native-core/hooks.d.ts +1 -1
  51. package/dist/react-native-core/hooks.d.ts.map +1 -1
  52. package/dist/react-native-core/index.js +3 -1
  53. package/dist/react-native-core/index.js.map +1 -1
  54. package/dist/testing.js +2 -2
  55. package/dist/testing.js.map +1 -1
  56. package/dist/tools/coValues/CoValueBase.d.ts +14 -0
  57. package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
  58. package/dist/tools/coValues/coMap.d.ts +0 -12
  59. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  60. package/dist/tools/coValues/inbox.d.ts +5 -5
  61. package/dist/tools/coValues/inbox.d.ts.map +1 -1
  62. package/dist/tools/implementation/createContext.d.ts +2 -1
  63. package/dist/tools/implementation/createContext.d.ts.map +1 -1
  64. package/dist/tools/tests/utils.d.ts.map +1 -1
  65. package/dist/worker/index.d.ts +12 -2
  66. package/dist/worker/index.d.ts.map +1 -1
  67. package/dist/worker/index.js +10 -4
  68. package/dist/worker/index.js.map +1 -1
  69. package/package.json +6 -4
  70. package/src/better-auth/auth/client.ts +8 -2
  71. package/src/better-auth/auth/react.tsx +2 -51
  72. package/src/better-auth/auth/server.ts +98 -24
  73. package/src/better-auth/auth/tests/client.test.ts +92 -4
  74. package/src/better-auth/auth/tests/react.test.tsx +43 -0
  75. package/src/better-auth/auth/tests/server.test.ts +276 -98
  76. package/src/inspector/custom-element.tsx +1 -1
  77. package/src/inspector/index.tsx +44 -0
  78. package/src/inspector/ui/button.tsx +15 -1
  79. package/src/inspector/ui/heading.tsx +7 -2
  80. package/src/inspector/ui/input.tsx +6 -2
  81. package/src/inspector/ui/modal.tsx +158 -0
  82. package/src/inspector/viewer/delete-local-data.tsx +101 -0
  83. package/src/inspector/viewer/new-app.tsx +3 -19
  84. package/src/react/hooks.tsx +1 -0
  85. package/src/react/index.ts +1 -0
  86. package/src/react-core/hooks.ts +162 -0
  87. package/src/react-core/tests/useCoStateWithSelector.test.ts +149 -0
  88. package/src/react-native-core/hooks.tsx +1 -0
  89. package/src/tools/coValues/CoValueBase.ts +32 -0
  90. package/src/tools/coValues/coList.ts +35 -0
  91. package/src/tools/coValues/coMap.ts +0 -18
  92. package/src/tools/coValues/inbox.ts +190 -108
  93. package/src/tools/implementation/createContext.ts +9 -2
  94. package/src/tools/testing.ts +1 -1
  95. package/src/tools/tests/coFeed.test.ts +33 -22
  96. package/src/tools/tests/coList.test.ts +47 -4
  97. package/src/tools/tests/coMap.test.ts +13 -5
  98. package/src/tools/tests/coPlainText.test.ts +24 -0
  99. package/src/tools/tests/createContext.test.ts +24 -0
  100. package/src/tools/tests/deepLoading.test.ts +2 -0
  101. package/src/tools/tests/exportImport.test.ts +3 -1
  102. package/src/tools/tests/groupsAndAccounts.test.ts +56 -44
  103. package/src/tools/tests/inbox.test.ts +293 -31
  104. package/src/tools/tests/patterns/requestToJoin.test.ts +14 -6
  105. package/src/tools/tests/utils.ts +1 -0
  106. package/src/worker/index.ts +21 -5
  107. package/tsup.config.ts +1 -1
  108. package/dist/chunk-3LE7N6TH.js.map +0 -1
  109. package/dist/inspector/custom-element-WCY6D3QJ.js.map +0 -1
  110. package/src/inspector/index.ts +0 -23
  111. /package/src/inspector/viewer/{inpsector-button.tsx → inspector-button.tsx} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/worker/index.ts"],"sourcesContent":["import { AgentSecret, CryptoProvider, LocalNode, Peer } from \"cojson\";\nimport {\n type AnyWebSocketConstructor,\n WebSocketPeerWithReconnection,\n} from \"cojson-transport-ws\";\nimport { WasmCrypto } from \"cojson/crypto/WasmCrypto\";\nimport {\n Account,\n AccountClass,\n AnyAccountSchema,\n CoValueFromRaw,\n Inbox,\n InstanceOfSchema,\n createJazzContextFromExistingCredentials,\n randomSessionProvider,\n} from \"jazz-tools\";\n\ntype WorkerOptions<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n> = {\n accountID?: string;\n accountSecret?: string;\n syncServer?: string;\n WebSocket?: AnyWebSocketConstructor;\n AccountSchema?: S;\n crypto?: CryptoProvider;\n};\n\n/** @category Context Creation */\nexport async function startWorker<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n>(options: WorkerOptions<S>) {\n const {\n accountID = process.env.JAZZ_WORKER_ACCOUNT,\n accountSecret = process.env.JAZZ_WORKER_SECRET,\n syncServer = \"wss://cloud.jazz.tools\",\n AccountSchema = Account as unknown as S,\n } = options;\n\n let node: LocalNode | undefined = undefined;\n\n const peersToLoadFrom: Peer[] = [];\n\n const wsPeer = new WebSocketPeerWithReconnection({\n peer: syncServer,\n reconnectionTimeout: 100,\n addPeer: (peer) => {\n if (node) {\n node.syncManager.addPeer(peer);\n } else {\n peersToLoadFrom.push(peer);\n }\n },\n removePeer: () => {},\n WebSocketConstructor: options.WebSocket,\n });\n\n wsPeer.enable();\n\n if (!accountID) {\n throw new Error(\"No accountID provided\");\n }\n if (!accountSecret) {\n throw new Error(\"No accountSecret provided\");\n }\n if (!accountID.startsWith(\"co_\")) {\n throw new Error(\"Invalid accountID\");\n }\n if (!accountSecret?.startsWith(\"sealerSecret_\")) {\n throw new Error(\"Invalid accountSecret\");\n }\n\n const context = await createJazzContextFromExistingCredentials({\n credentials: {\n accountID: accountID,\n secret: accountSecret as AgentSecret,\n },\n AccountSchema,\n // TODO: locked sessions similar to browser\n sessionProvider: randomSessionProvider,\n peersToLoadFrom,\n crypto: options.crypto ?? (await WasmCrypto.create()),\n });\n\n const account = context.account as InstanceOfSchema<S>;\n node = account.$jazz.localNode;\n\n if (!account.$jazz.refs.profile?.id) {\n throw new Error(\"Account has no profile\");\n }\n\n const inbox = await Inbox.load(account);\n\n async function done() {\n await context.account.$jazz.waitForAllCoValuesSync();\n\n wsPeer.disable();\n context.done();\n }\n\n const inboxPublicApi = {\n subscribe: inbox.subscribe.bind(inbox) as Inbox[\"subscribe\"],\n };\n\n return {\n worker: context.account as InstanceOfSchema<S>,\n experimental: {\n inbox: inboxPublicApi,\n },\n waitForConnection() {\n return wsPeer.waitUntilConnected();\n },\n subscribeToConnectionChange(listener: (connected: boolean) => void) {\n wsPeer.subscribe(listener);\n\n return () => {\n wsPeer.unsubscribe(listener);\n };\n },\n done,\n };\n}\n"],"mappings":";AACA;AAAA,EAEE;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EAIA;AAAA,EAEA;AAAA,EACA;AAAA,OACK;AAgBP,eAAsB,YAIpB,SAA2B;AAC3B,QAAM;AAAA,IACJ,YAAY,QAAQ,IAAI;AAAA,IACxB,gBAAgB,QAAQ,IAAI;AAAA,IAC5B,aAAa;AAAA,IACb,gBAAgB;AAAA,EAClB,IAAI;AAEJ,MAAI,OAA8B;AAElC,QAAM,kBAA0B,CAAC;AAEjC,QAAM,SAAS,IAAI,8BAA8B;AAAA,IAC/C,MAAM;AAAA,IACN,qBAAqB;AAAA,IACrB,SAAS,CAAC,SAAS;AACjB,UAAI,MAAM;AACR,aAAK,YAAY,QAAQ,IAAI;AAAA,MAC/B,OAAO;AACL,wBAAgB,KAAK,IAAI;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,YAAY,MAAM;AAAA,IAAC;AAAA,IACnB,sBAAsB,QAAQ;AAAA,EAChC,CAAC;AAED,SAAO,OAAO;AAEd,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AACA,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,2BAA2B;AAAA,EAC7C;AACA,MAAI,CAAC,UAAU,WAAW,KAAK,GAAG;AAChC,UAAM,IAAI,MAAM,mBAAmB;AAAA,EACrC;AACA,MAAI,CAAC,eAAe,WAAW,eAAe,GAAG;AAC/C,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AAEA,QAAM,UAAU,MAAM,yCAAyC;AAAA,IAC7D,aAAa;AAAA,MACX;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,IACA;AAAA;AAAA,IAEA,iBAAiB;AAAA,IACjB;AAAA,IACA,QAAQ,QAAQ,UAAW,MAAM,WAAW,OAAO;AAAA,EACrD,CAAC;AAED,QAAM,UAAU,QAAQ;AACxB,SAAO,QAAQ,MAAM;AAErB,MAAI,CAAC,QAAQ,MAAM,KAAK,SAAS,IAAI;AACnC,UAAM,IAAI,MAAM,wBAAwB;AAAA,EAC1C;AAEA,QAAM,QAAQ,MAAM,MAAM,KAAK,OAAO;AAEtC,iBAAe,OAAO;AACpB,UAAM,QAAQ,QAAQ,MAAM,uBAAuB;AAEnD,WAAO,QAAQ;AACf,YAAQ,KAAK;AAAA,EACf;AAEA,QAAM,iBAAiB;AAAA,IACrB,WAAW,MAAM,UAAU,KAAK,KAAK;AAAA,EACvC;AAEA,SAAO;AAAA,IACL,QAAQ,QAAQ;AAAA,IAChB,cAAc;AAAA,MACZ,OAAO;AAAA,IACT;AAAA,IACA,oBAAoB;AAClB,aAAO,OAAO,mBAAmB;AAAA,IACnC;AAAA,IACA,4BAA4B,UAAwC;AAClE,aAAO,UAAU,QAAQ;AAEzB,aAAO,MAAM;AACX,eAAO,YAAY,QAAQ;AAAA,MAC7B;AAAA,IACF;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/worker/index.ts"],"sourcesContent":["import { AgentSecret, CryptoProvider, LocalNode, Peer } from \"cojson\";\nimport {\n type AnyWebSocketConstructor,\n WebSocketPeerWithReconnection,\n} from \"cojson-transport-ws\";\nimport { WasmCrypto } from \"cojson/crypto/WasmCrypto\";\nimport {\n Account,\n AccountClass,\n AnyAccountSchema,\n CoValueFromRaw,\n Inbox,\n InstanceOfSchema,\n Loaded,\n createJazzContextFromExistingCredentials,\n randomSessionProvider,\n} from \"jazz-tools\";\n\ntype WorkerOptions<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n> = {\n accountID?: string;\n accountSecret?: string;\n syncServer?: string;\n WebSocket?: AnyWebSocketConstructor;\n AccountSchema?: S;\n crypto?: CryptoProvider;\n /**\n * If true, the inbox will not be loaded.\n */\n skipInboxLoad?: boolean;\n /**\n * If false, the worker will not set in the global account context\n */\n asActiveAccount?: boolean;\n};\n\n/** @category Context Creation */\nexport async function startWorker<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n>(options: WorkerOptions<S>) {\n const {\n accountID = process.env.JAZZ_WORKER_ACCOUNT,\n accountSecret = process.env.JAZZ_WORKER_SECRET,\n syncServer = \"wss://cloud.jazz.tools\",\n AccountSchema = Account as unknown as S,\n skipInboxLoad = false,\n asActiveAccount = true,\n } = options;\n\n let node: LocalNode | undefined = undefined;\n\n const peersToLoadFrom: Peer[] = [];\n\n const wsPeer = new WebSocketPeerWithReconnection({\n peer: syncServer,\n reconnectionTimeout: 100,\n addPeer: (peer) => {\n if (node) {\n node.syncManager.addPeer(peer);\n } else {\n peersToLoadFrom.push(peer);\n }\n },\n removePeer: () => {},\n WebSocketConstructor: options.WebSocket,\n });\n\n wsPeer.enable();\n\n if (!accountID) {\n throw new Error(\"No accountID provided\");\n }\n if (!accountSecret) {\n throw new Error(\"No accountSecret provided\");\n }\n if (!accountID.startsWith(\"co_\")) {\n throw new Error(\"Invalid accountID\");\n }\n if (!accountSecret?.startsWith(\"sealerSecret_\")) {\n throw new Error(\"Invalid accountSecret\");\n }\n\n const context = await createJazzContextFromExistingCredentials({\n credentials: {\n accountID: accountID,\n secret: accountSecret as AgentSecret,\n },\n AccountSchema,\n // TODO: locked sessions similar to browser\n sessionProvider: randomSessionProvider,\n peersToLoadFrom,\n crypto: options.crypto ?? (await WasmCrypto.create()),\n asActiveAccount,\n });\n\n const account = context.account as InstanceOfSchema<S>;\n node = account.$jazz.localNode;\n\n if (!account.$jazz.refs.profile?.id) {\n throw new Error(\"Account has no profile\");\n }\n\n const inbox = skipInboxLoad ? undefined : await Inbox.load(account);\n\n async function done() {\n await context.account.$jazz.waitForAllCoValuesSync();\n\n wsPeer.disable();\n context.done();\n }\n\n const inboxPublicApi = inbox\n ? {\n subscribe: inbox.subscribe.bind(inbox) as Inbox[\"subscribe\"],\n }\n : {\n subscribe: () => {},\n };\n\n return {\n worker: context.account as Loaded<S>,\n experimental: {\n inbox: inboxPublicApi,\n },\n waitForConnection() {\n return wsPeer.waitUntilConnected();\n },\n subscribeToConnectionChange(listener: (connected: boolean) => void) {\n wsPeer.subscribe(listener);\n\n return () => {\n wsPeer.unsubscribe(listener);\n };\n },\n done,\n };\n}\n"],"mappings":";AACA;AAAA,EAEE;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EAIA;AAAA,EAGA;AAAA,EACA;AAAA,OACK;AAwBP,eAAsB,YAIpB,SAA2B;AAC3B,QAAM;AAAA,IACJ,YAAY,QAAQ,IAAI;AAAA,IACxB,gBAAgB,QAAQ,IAAI;AAAA,IAC5B,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,kBAAkB;AAAA,EACpB,IAAI;AAEJ,MAAI,OAA8B;AAElC,QAAM,kBAA0B,CAAC;AAEjC,QAAM,SAAS,IAAI,8BAA8B;AAAA,IAC/C,MAAM;AAAA,IACN,qBAAqB;AAAA,IACrB,SAAS,CAAC,SAAS;AACjB,UAAI,MAAM;AACR,aAAK,YAAY,QAAQ,IAAI;AAAA,MAC/B,OAAO;AACL,wBAAgB,KAAK,IAAI;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,YAAY,MAAM;AAAA,IAAC;AAAA,IACnB,sBAAsB,QAAQ;AAAA,EAChC,CAAC;AAED,SAAO,OAAO;AAEd,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AACA,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,2BAA2B;AAAA,EAC7C;AACA,MAAI,CAAC,UAAU,WAAW,KAAK,GAAG;AAChC,UAAM,IAAI,MAAM,mBAAmB;AAAA,EACrC;AACA,MAAI,CAAC,eAAe,WAAW,eAAe,GAAG;AAC/C,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AAEA,QAAM,UAAU,MAAM,yCAAyC;AAAA,IAC7D,aAAa;AAAA,MACX;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,IACA;AAAA;AAAA,IAEA,iBAAiB;AAAA,IACjB;AAAA,IACA,QAAQ,QAAQ,UAAW,MAAM,WAAW,OAAO;AAAA,IACnD;AAAA,EACF,CAAC;AAED,QAAM,UAAU,QAAQ;AACxB,SAAO,QAAQ,MAAM;AAErB,MAAI,CAAC,QAAQ,MAAM,KAAK,SAAS,IAAI;AACnC,UAAM,IAAI,MAAM,wBAAwB;AAAA,EAC1C;AAEA,QAAM,QAAQ,gBAAgB,SAAY,MAAM,MAAM,KAAK,OAAO;AAElE,iBAAe,OAAO;AACpB,UAAM,QAAQ,QAAQ,MAAM,uBAAuB;AAEnD,WAAO,QAAQ;AACf,YAAQ,KAAK;AAAA,EACf;AAEA,QAAM,iBAAiB,QACnB;AAAA,IACE,WAAW,MAAM,UAAU,KAAK,KAAK;AAAA,EACvC,IACA;AAAA,IACE,WAAW,MAAM;AAAA,IAAC;AAAA,EACpB;AAEJ,SAAO;AAAA,IACL,QAAQ,QAAQ;AAAA,IAChB,cAAc;AAAA,MACZ,OAAO;AAAA,IACT;AAAA,IACA,oBAAoB;AAClB,aAAO,OAAO,mBAAmB;AAAA,IACnC;AAAA,IACA,4BAA4B,UAAwC;AAClE,aAAO,UAAU,QAAQ;AAEzB,aAAO,MAAM;AACX,eAAO,YAAY,QAAQ;AAAA,MAC7B;AAAA,IACF;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -167,7 +167,7 @@
167
167
  },
168
168
  "type": "module",
169
169
  "license": "MIT",
170
- "version": "0.18.5",
170
+ "version": "0.18.7",
171
171
  "dependencies": {
172
172
  "@manuscripts/prosemirror-recreate-steps": "^0.1.4",
173
173
  "@scure/base": "1.2.1",
@@ -182,10 +182,11 @@
182
182
  "prosemirror-schema-basic": "^1.2.2",
183
183
  "prosemirror-state": "^1.4.3",
184
184
  "prosemirror-transform": "^1.9.0",
185
+ "use-sync-external-store": "^1.5.0",
185
186
  "zod": "3.25.76",
186
- "cojson": "0.18.5",
187
- "cojson-storage-indexeddb": "0.18.5",
188
- "cojson-transport-ws": "0.18.5"
187
+ "cojson": "0.18.7",
188
+ "cojson-storage-indexeddb": "0.18.7",
189
+ "cojson-transport-ws": "0.18.7"
189
190
  },
190
191
  "devDependencies": {
191
192
  "@scure/bip39": "^1.3.0",
@@ -197,6 +198,7 @@
197
198
  "@testing-library/svelte": "^5.2.6",
198
199
  "@types/react": "19.1.0",
199
200
  "@types/react-dom": "19.1.0",
201
+ "@types/use-sync-external-store": "^1.5.0",
200
202
  "@vitest/browser": "^3.2.4",
201
203
  "msw": "^2.10.3",
202
204
  "oauth2-mock-server": "^8.1.0",
@@ -7,6 +7,13 @@ import type {
7
7
  } from "jazz-tools";
8
8
  import type { jazzPlugin } from "./server.js";
9
9
 
10
+ const SIGNUP_URLS = [
11
+ "/sign-up",
12
+ "/sign-in/social",
13
+ "/sign-in/oauth2",
14
+ "/email-otp/send-verification-otp",
15
+ ];
16
+
10
17
  /**
11
18
  * @example
12
19
  * ```ts
@@ -83,8 +90,7 @@ export const jazzPluginClient = () => {
83
90
  hooks: {
84
91
  async onRequest(context) {
85
92
  if (
86
- context.url.toString().includes("/sign-up") ||
87
- context.url.toString().includes("/sign-in/social")
93
+ SIGNUP_URLS.some((url) => context.url.toString().includes(url))
88
94
  ) {
89
95
  const credentials = await authSecretStorage.get();
90
96
 
@@ -1,21 +1,9 @@
1
1
  "use client";
2
2
 
3
- import type { ClientOptions } from "better-auth";
4
3
  import { createAuthClient } from "better-auth/client";
5
- import type {
6
- Account,
7
- AccountClass,
8
- AnyAccountSchema,
9
- CoValueFromRaw,
10
- } from "jazz-tools";
11
- import {
12
- type JazzProviderProps,
13
- JazzReactProvider,
14
- useAuthSecretStorage,
15
- useJazzContext,
16
- } from "jazz-tools/react";
4
+ import { useAuthSecretStorage, useJazzContext } from "jazz-tools/react-core";
17
5
  import { useEffect } from "react";
18
- import { type PropsWithChildren, createContext } from "react";
6
+ import { type PropsWithChildren } from "react";
19
7
  import { jazzPluginClient } from "./client.js";
20
8
 
21
9
  type AuthClient = ReturnType<
@@ -24,8 +12,6 @@ type AuthClient = ReturnType<
24
12
  }>
25
13
  >;
26
14
 
27
- export const AuthContext = createContext<AuthClient | null>(null);
28
-
29
15
  /**
30
16
  * @param props.children - The children to render.
31
17
  * @param props.betterAuthClient - The BetterAuth client with the Jazz plugin.
@@ -68,38 +54,3 @@ export function AuthProvider({
68
54
 
69
55
  return children;
70
56
  }
71
-
72
- /**
73
- * @param props - The props for the JazzReactProvider.
74
- * @param props.betterAuth - The options for the BetterAuth client.
75
- * @returns The JazzReactProvider with the BetterAuth plugin.
76
- *
77
- * @example
78
- * ```ts
79
- * <JazzReactProviderWithBetterAuth
80
- * betterAuth={{
81
- * baseURL: "http://localhost:3000",
82
- * }}
83
- * sync={{
84
- * peer: "ws://localhost:4200",
85
- * }}
86
- * >
87
- * <App />
88
- * </JazzReactProviderWithBetterAuth>
89
- * ```
90
- */
91
- export const JazzReactProviderWithBetterAuth = <
92
- S extends
93
- | (AccountClass<Account> & CoValueFromRaw<Account>)
94
- | AnyAccountSchema,
95
- >(
96
- props: { betterAuthClient: AuthClient } & JazzProviderProps<S>,
97
- ) => {
98
- return (
99
- <JazzReactProvider {...props}>
100
- <AuthProvider betterAuthClient={props.betterAuthClient}>
101
- {props.children}
102
- </AuthProvider>
103
- </JazzReactProvider>
104
- );
105
- };
@@ -85,20 +85,27 @@ export const jazzPlugin: () => JazzPlugin = () => {
85
85
  },
86
86
  verification: {
87
87
  create: {
88
- before: async (verification, context) => {
89
- // If a jazzAuth is provided, save it for later usage.
90
- if (contextContainsJazzAuth(context)) {
91
- const parsed = JSON.parse(verification.value);
92
- const newValue = JSON.stringify({
93
- ...parsed,
94
- jazzAuth: context.jazzAuth,
95
- });
96
-
97
- return {
98
- data: {
99
- value: newValue,
88
+ after: async (verification, context) => {
89
+ /**
90
+ * For: Email OTP plugin
91
+ * After a verification is created, if it is from the EmailOTP plugin,
92
+ * create a new verification value with the jazzAuth with the same expiration.
93
+ */
94
+ if (
95
+ contextContainsJazzAuth(context) &&
96
+ verification.identifier.startsWith("sign-in-otp-")
97
+ ) {
98
+ const identifier = `jazz-auth-${verification.identifier}`;
99
+ await context.context.internalAdapter.deleteVerificationValue(
100
+ identifier,
101
+ );
102
+ await context.context.internalAdapter.createVerificationValue(
103
+ {
104
+ value: JSON.stringify({ jazzAuth: context.jazzAuth }),
105
+ identifier: identifier,
106
+ expiresAt: verification.expiresAt,
100
107
  },
101
- };
108
+ );
102
109
  }
103
110
  },
104
111
  },
@@ -147,6 +154,7 @@ export const jazzPlugin: () => JazzPlugin = () => {
147
154
  },
148
155
 
149
156
  /**
157
+ * For: Social / OAuth2 plugin
150
158
  * /callback is the endpoint that BetterAuth uses to authenticate the user coming from a social provider.
151
159
  * 1. Catch the state
152
160
  * 2. Find the verification value
@@ -162,17 +170,12 @@ export const jazzPlugin: () => JazzPlugin = () => {
162
170
  handler: createAuthMiddleware(async (ctx) => {
163
171
  const state = ctx.query?.state || ctx.body?.state;
164
172
 
165
- const data = await ctx.context.adapter.findOne<{ value: string }>({
166
- model: ctx.context.tables.verification!.modelName,
167
- where: [
168
- {
169
- field: "identifier",
170
- operator: "eq",
171
- value: state,
172
- },
173
- ],
174
- select: ["value"],
175
- });
173
+ const identifier = `jazz-auth-${state}`;
174
+
175
+ const data =
176
+ await ctx.context.internalAdapter.findVerificationValue(
177
+ identifier,
178
+ );
176
179
 
177
180
  // if not found, the social plugin will throw later anyway
178
181
  if (!data) {
@@ -197,6 +200,45 @@ export const jazzPlugin: () => JazzPlugin = () => {
197
200
  }
198
201
  }),
199
202
  },
203
+ /**
204
+ * For: Email OTP plugin
205
+ * When the user sends an OTP, we try to find the jazzAuth.
206
+ * If it isn't a sign-up, we expect to not find a verification value.
207
+ */
208
+ {
209
+ matcher: (context) => {
210
+ return context.path.startsWith("/sign-in/email-otp");
211
+ },
212
+ handler: createAuthMiddleware(async (ctx) => {
213
+ const email = ctx.body.email;
214
+ const identifier = `jazz-auth-sign-in-otp-${email}`;
215
+
216
+ const data =
217
+ await ctx.context.internalAdapter.findVerificationValue(
218
+ identifier,
219
+ );
220
+
221
+ // if not found, it isn't a sign-up
222
+ if (!data || data.expiresAt < new Date()) {
223
+ return;
224
+ }
225
+
226
+ const parsed = JSON.parse(data.value);
227
+
228
+ if (parsed && "jazzAuth" in parsed) {
229
+ return {
230
+ context: {
231
+ ...ctx,
232
+ jazzAuth: parsed.jazzAuth,
233
+ },
234
+ };
235
+ } else {
236
+ throw new APIError(500, {
237
+ message: "JazzAuth not found in verification value",
238
+ });
239
+ }
240
+ }),
241
+ },
200
242
  ],
201
243
  after: [
202
244
  /**
@@ -227,6 +269,38 @@ export const jazzPlugin: () => JazzPlugin = () => {
227
269
  });
228
270
  }),
229
271
  },
272
+
273
+ /**
274
+ * For: Social / OAuth2 plugin
275
+ * When the user sign-in via social, we create a verification value with the jazzAuth.
276
+ */
277
+ {
278
+ matcher: (context) => {
279
+ return context.path.startsWith("/sign-in/social");
280
+ },
281
+ handler: createAuthMiddleware(async (ctx) => {
282
+ if (!contextContainsJazzAuth(ctx)) {
283
+ throw new APIError(500, {
284
+ message: "JazzAuth not found in context",
285
+ });
286
+ }
287
+
288
+ const returned = ctx.context.returned as { url: string };
289
+
290
+ const url = new URL(returned.url);
291
+ const state = url.searchParams.get("state");
292
+
293
+ const value = JSON.stringify({ jazzAuth: ctx.jazzAuth });
294
+ const expiresAt = new Date();
295
+ expiresAt.setMinutes(expiresAt.getMinutes() + 10);
296
+
297
+ await ctx.context.internalAdapter.createVerificationValue({
298
+ value,
299
+ identifier: `jazz-auth-${state}`,
300
+ expiresAt,
301
+ });
302
+ }),
303
+ },
230
304
  ],
231
305
  },
232
306
  } satisfies JazzPlugin;
@@ -7,14 +7,19 @@ import {
7
7
  } from "jazz-tools/testing";
8
8
  import { assert, beforeEach, describe, expect, it, vi } from "vitest";
9
9
  import { jazzPluginClient } from "../client.js";
10
+ import { emailOTPClient, genericOAuthClient } from "better-auth/client/plugins";
10
11
 
11
- describe("auth client", () => {
12
+ describe("Better-Auth client plugin", () => {
12
13
  let account: Account;
13
14
  let jazzContextManager: TestJazzContextManager<Account>;
14
15
  let authSecretStorage: AuthSecretStorage;
15
16
  let authClient: ReturnType<
16
17
  typeof createAuthClient<{
17
- plugins: ReturnType<typeof jazzPluginClient>[];
18
+ plugins: ReturnType<
19
+ | typeof jazzPluginClient
20
+ | typeof emailOTPClient
21
+ | typeof genericOAuthClient
22
+ >[];
18
23
  }>
19
24
  >;
20
25
  let customFetchImpl = vi.fn();
@@ -31,7 +36,7 @@ describe("auth client", () => {
31
36
 
32
37
  authClient = createAuthClient({
33
38
  baseURL: "http://localhost:3000",
34
- plugins: [jazzPluginClient()],
39
+ plugins: [jazzPluginClient(), emailOTPClient(), genericOAuthClient()],
35
40
  fetchOptions: {
36
41
  customFetchImpl,
37
42
  },
@@ -245,5 +250,88 @@ describe("auth client", () => {
245
250
  expect(anonymousCredentials).not.toMatchObject(credentials!);
246
251
  });
247
252
 
248
- it.todo("should logout from Better Auth after Jazz's log-out");
253
+ it("should send Jazz credentials using social login", async () => {
254
+ const credentials = await authSecretStorage.get();
255
+ assert(credentials, "Jazz credentials are not available");
256
+
257
+ customFetchImpl.mockResolvedValue(new Response(JSON.stringify({})));
258
+
259
+ // Sign up
260
+ await authClient.signIn.social({
261
+ provider: "github",
262
+ });
263
+
264
+ expect(customFetchImpl).toHaveBeenCalledTimes(1);
265
+ expect(customFetchImpl.mock.calls[0]![0].toString()).toBe(
266
+ "http://localhost:3000/api/auth/sign-in/social",
267
+ );
268
+
269
+ // Verify the credentials have been injected in the request body
270
+ expect(
271
+ customFetchImpl.mock.calls[0]![1].headers.get("x-jazz-auth")!,
272
+ ).toEqual(
273
+ JSON.stringify({
274
+ accountID: credentials!.accountID,
275
+ secretSeed: credentials!.secretSeed,
276
+ accountSecret: credentials!.accountSecret,
277
+ }),
278
+ );
279
+ });
280
+
281
+ it("should send Jazz credentials using oauth generic plugin", async () => {
282
+ const credentials = await authSecretStorage.get();
283
+ assert(credentials, "Jazz credentials are not available");
284
+
285
+ customFetchImpl.mockResolvedValue(new Response(JSON.stringify({})));
286
+
287
+ // Sign up
288
+ await authClient.signIn.oauth2({
289
+ providerId: "github",
290
+ });
291
+
292
+ expect(customFetchImpl).toHaveBeenCalledTimes(1);
293
+ expect(customFetchImpl.mock.calls[0]![0].toString()).toBe(
294
+ "http://localhost:3000/api/auth/sign-in/oauth2",
295
+ );
296
+
297
+ // Verify the credentials have been injected in the request body
298
+ expect(
299
+ customFetchImpl.mock.calls[0]![1].headers.get("x-jazz-auth")!,
300
+ ).toEqual(
301
+ JSON.stringify({
302
+ accountID: credentials!.accountID,
303
+ secretSeed: credentials!.secretSeed,
304
+ accountSecret: credentials!.accountSecret,
305
+ }),
306
+ );
307
+ });
308
+
309
+ it("should send Jazz credentials using email OTP", async () => {
310
+ const credentials = await authSecretStorage.get();
311
+ assert(credentials, "Jazz credentials are not available");
312
+
313
+ customFetchImpl.mockResolvedValue(new Response(JSON.stringify({})));
314
+
315
+ // Sign up
316
+ await authClient.emailOtp.sendVerificationOtp({
317
+ email: "test@jazz.dev",
318
+ type: "sign-in",
319
+ });
320
+
321
+ expect(customFetchImpl).toHaveBeenCalledTimes(1);
322
+ expect(customFetchImpl.mock.calls[0]![0].toString()).toBe(
323
+ "http://localhost:3000/api/auth/email-otp/send-verification-otp",
324
+ );
325
+
326
+ // Verify the credentials have been injected in the request body
327
+ expect(
328
+ customFetchImpl.mock.calls[0]![1].headers.get("x-jazz-auth")!,
329
+ ).toEqual(
330
+ JSON.stringify({
331
+ accountID: credentials!.accountID,
332
+ secretSeed: credentials!.secretSeed,
333
+ accountSecret: credentials!.accountSecret,
334
+ }),
335
+ );
336
+ });
249
337
  });
@@ -0,0 +1,43 @@
1
+ // @vitest-environment jsdom
2
+ import { render } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { AuthProvider } from "../react";
5
+ import { createAuthClient } from "better-auth/client";
6
+ import { jazzPluginClient } from "../client";
7
+ import { JazzReactProvider } from "jazz-tools/react";
8
+
9
+ describe("AuthProvider", () => {
10
+ it("should throw if no JazzContext is set", () => {
11
+ const betterAuthClient = createAuthClient({
12
+ plugins: [jazzPluginClient()],
13
+ });
14
+
15
+ expect(() => {
16
+ render(
17
+ <AuthProvider betterAuthClient={betterAuthClient}>
18
+ <div />
19
+ </AuthProvider>,
20
+ );
21
+ }).toThrow(
22
+ "You need to set up a JazzProvider on top of your app to use this hook.",
23
+ );
24
+ });
25
+
26
+ it("should render with JazzReactProvider", () => {
27
+ const betterAuthClient = createAuthClient({
28
+ plugins: [jazzPluginClient()],
29
+ });
30
+
31
+ render(
32
+ <JazzReactProvider
33
+ // @ts-expect-error - no memory storage
34
+ storage={["memory"]}
35
+ sync={{ peer: "ws://", when: "never" }}
36
+ >
37
+ <AuthProvider betterAuthClient={betterAuthClient}>
38
+ <div />
39
+ </AuthProvider>
40
+ </JazzReactProvider>,
41
+ );
42
+ });
43
+ });