@carlonicora/nextjs-jsonapi 6.21.0 → 6.23.0

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 (182) hide show
  1. package/dist/{BlockNoteEditor-NXCRWS2O.js → BlockNoteEditor-PPRML5CO.js} +23 -23
  2. package/dist/{BlockNoteEditor-NXCRWS2O.js.map → BlockNoteEditor-PPRML5CO.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-66ZACETR.mjs → BlockNoteEditor-UO4ZRCUM.mjs} +8 -8
  4. package/dist/{BlockNoteViewer-2UDFTXRS.mjs → BlockNoteViewer-2TJZ6JET.mjs} +6 -6
  5. package/dist/{BlockNoteViewer-4OHBRE4Q.js → BlockNoteViewer-K2HBF3IP.js} +7 -7
  6. package/dist/{BlockNoteViewer-4OHBRE4Q.js.map → BlockNoteViewer-K2HBF3IP.js.map} +1 -1
  7. package/dist/{JsonApiRequest-IKKHWCPB.mjs → JsonApiRequest-ADLBJ5RX.mjs} +2 -2
  8. package/dist/JsonApiRequest-KC6DOHKT.js +26 -0
  9. package/dist/{JsonApiRequest-G4J2WTSU.js.map → JsonApiRequest-KC6DOHKT.js.map} +1 -1
  10. package/dist/billing/index.js +495 -495
  11. package/dist/billing/index.mjs +7 -7
  12. package/dist/{chunk-N5ZE2Z6I.js → chunk-53SD4YRF.js} +5 -5
  13. package/dist/{chunk-N5ZE2Z6I.js.map → chunk-53SD4YRF.js.map} +1 -1
  14. package/dist/{chunk-4RA5PRXX.mjs → chunk-7OIWOPM5.mjs} +2 -2
  15. package/dist/{chunk-WCEYT3GG.js → chunk-A5M3MORY.js} +6 -3
  16. package/dist/chunk-A5M3MORY.js.map +1 -0
  17. package/dist/{chunk-AUD6ZLBB.mjs → chunk-BE6UMM5X.mjs} +13 -8
  18. package/dist/chunk-BE6UMM5X.mjs.map +1 -0
  19. package/dist/{chunk-KV4N7DEG.js → chunk-BQ2WAG4K.js} +29 -24
  20. package/dist/chunk-BQ2WAG4K.js.map +1 -0
  21. package/dist/{chunk-OQQ7CZWT.js → chunk-GMMDXINC.js} +12 -12
  22. package/dist/{chunk-OQQ7CZWT.js.map → chunk-GMMDXINC.js.map} +1 -1
  23. package/dist/{chunk-ZB6OYIMB.js → chunk-HN7L6RRW.js} +1296 -1093
  24. package/dist/{chunk-DPXOPLQP.mjs.map → chunk-HN7L6RRW.js.map} +1 -1
  25. package/dist/{chunk-OHHA5BBX.mjs → chunk-HP6DT6K3.mjs} +17 -15
  26. package/dist/chunk-HP6DT6K3.mjs.map +1 -0
  27. package/dist/{chunk-JSHLBRFV.mjs → chunk-N2BFT7KI.mjs} +2 -2
  28. package/dist/chunk-N2BFT7KI.mjs.map +1 -0
  29. package/dist/{chunk-YWLSIIE6.js → chunk-P7L6OFX5.js} +11 -11
  30. package/dist/{chunk-YWLSIIE6.js.map → chunk-P7L6OFX5.js.map} +1 -1
  31. package/dist/{chunk-3CGU6WXY.js → chunk-R5722NLL.js} +926 -924
  32. package/dist/chunk-R5722NLL.js.map +1 -0
  33. package/dist/{chunk-GM4VZTWL.js → chunk-RCOKQ3R4.js} +14 -8
  34. package/dist/chunk-RCOKQ3R4.js.map +1 -0
  35. package/dist/{chunk-M3N7LPHX.mjs → chunk-S7SPAC6X.mjs} +6 -3
  36. package/dist/chunk-S7SPAC6X.mjs.map +1 -0
  37. package/dist/{chunk-54K2CZ2F.js → chunk-SO3YUCL6.js} +4 -4
  38. package/dist/{chunk-54K2CZ2F.js.map → chunk-SO3YUCL6.js.map} +1 -1
  39. package/dist/{chunk-DPXOPLQP.mjs → chunk-T6G2F2BK.mjs} +220 -17
  40. package/dist/chunk-T6G2F2BK.mjs.map +1 -0
  41. package/dist/{chunk-G2UIQSNJ.mjs → chunk-TMV7TL7O.mjs} +4 -4
  42. package/dist/{chunk-BD6ARGGI.js → chunk-TNTCLVUR.js} +15 -15
  43. package/dist/{chunk-BD6ARGGI.js.map → chunk-TNTCLVUR.js.map} +1 -1
  44. package/dist/{chunk-QIEF7HIL.mjs → chunk-VNUT2D27.mjs} +2 -2
  45. package/dist/{chunk-KTGKW72G.mjs → chunk-W4ML6D2X.mjs} +2 -2
  46. package/dist/{chunk-MQ7TXALH.mjs → chunk-YMCL3JXZ.mjs} +14 -8
  47. package/dist/chunk-YMCL3JXZ.mjs.map +1 -0
  48. package/dist/{chunk-QGVVF77L.js → chunk-YSE2Z4RB.js} +11 -11
  49. package/dist/{chunk-QGVVF77L.js.map → chunk-YSE2Z4RB.js.map} +1 -1
  50. package/dist/{chunk-A3RB47X4.mjs → chunk-YTDGYI62.mjs} +3 -3
  51. package/dist/client/JsonApiClient.d.ts +5 -0
  52. package/dist/client/JsonApiClient.d.ts.map +1 -1
  53. package/dist/client/index.js +8 -8
  54. package/dist/client/index.mjs +7 -7
  55. package/dist/client/request.d.ts +5 -0
  56. package/dist/client/request.d.ts.map +1 -1
  57. package/dist/components/index.js +8 -8
  58. package/dist/components/index.mjs +7 -7
  59. package/dist/contexts/index.js +8 -8
  60. package/dist/contexts/index.mjs +7 -7
  61. package/dist/core/abstracts/AbstractService.d.ts +5 -0
  62. package/dist/core/abstracts/AbstractService.d.ts.map +1 -1
  63. package/dist/core/abstracts/ClientAbstractService.d.ts +5 -0
  64. package/dist/core/abstracts/ClientAbstractService.d.ts.map +1 -1
  65. package/dist/core/index.d.ts +2 -0
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +24 -6
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/index.mjs +23 -5
  70. package/dist/core/registry/ModuleRegistry.d.ts +3 -0
  71. package/dist/core/registry/ModuleRegistry.d.ts.map +1 -1
  72. package/dist/email-automations/index.js +121 -121
  73. package/dist/email-automations/index.mjs +7 -7
  74. package/dist/features/analytics/index.js +139 -139
  75. package/dist/features/analytics/index.mjs +7 -7
  76. package/dist/features/analytics/server.js +9 -9
  77. package/dist/features/analytics/server.mjs +7 -7
  78. package/dist/features/blog/index.js +154 -154
  79. package/dist/features/blog/index.mjs +7 -7
  80. package/dist/features/blog/server.js +27 -27
  81. package/dist/features/blog/server.mjs +6 -6
  82. package/dist/features/discord-connection/data/discord-connect-confirmation.d.ts +11 -0
  83. package/dist/features/discord-connection/data/discord-connect-confirmation.d.ts.map +1 -0
  84. package/dist/features/discord-connection/data/discord-connect-confirmation.interface.d.ts +14 -0
  85. package/dist/features/discord-connection/data/discord-connect-confirmation.interface.d.ts.map +1 -0
  86. package/dist/features/discord-connection/data/discord-connect-confirmation.service.d.ts +10 -0
  87. package/dist/features/discord-connection/data/discord-connect-confirmation.service.d.ts.map +1 -0
  88. package/dist/features/discord-connection/data/discord-connect-intent.d.ts +11 -0
  89. package/dist/features/discord-connection/data/discord-connect-intent.d.ts.map +1 -0
  90. package/dist/features/discord-connection/data/discord-connect-intent.interface.d.ts +15 -0
  91. package/dist/features/discord-connection/data/discord-connect-intent.interface.d.ts.map +1 -0
  92. package/dist/features/discord-connection/data/discord-connect-intent.service.d.ts +11 -0
  93. package/dist/features/discord-connection/data/discord-connect-intent.service.d.ts.map +1 -0
  94. package/dist/features/discord-connection/data/discord-connection.d.ts +14 -0
  95. package/dist/features/discord-connection/data/discord-connection.d.ts.map +1 -0
  96. package/dist/features/discord-connection/data/discord-connection.interface.d.ts +10 -0
  97. package/dist/features/discord-connection/data/discord-connection.interface.d.ts.map +1 -0
  98. package/dist/features/discord-connection/data/discord-connection.service.d.ts +12 -0
  99. package/dist/features/discord-connection/data/discord-connection.service.d.ts.map +1 -0
  100. package/dist/features/discord-connection/data/index.d.ts +10 -0
  101. package/dist/features/discord-connection/data/index.d.ts.map +1 -0
  102. package/dist/features/discord-connection/discord-connection.module.d.ts +5 -0
  103. package/dist/features/discord-connection/discord-connection.module.d.ts.map +1 -0
  104. package/dist/features/discord-connection/index.d.ts +3 -0
  105. package/dist/features/discord-connection/index.d.ts.map +1 -0
  106. package/dist/features/help/index.js +41 -41
  107. package/dist/features/help/index.mjs +7 -7
  108. package/dist/features/tokenusage/index.js +130 -130
  109. package/dist/features/tokenusage/index.mjs +7 -7
  110. package/dist/features/user/contexts/UserContext.d.ts.map +1 -1
  111. package/dist/index.d.ts +1 -1
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +27 -7
  114. package/dist/index.js.map +1 -1
  115. package/dist/index.mjs +26 -6
  116. package/dist/local-transport-MJPOKPTU.js +15 -0
  117. package/dist/{local-transport-4YDJ6FQG.js.map → local-transport-MJPOKPTU.js.map} +1 -1
  118. package/dist/{local-transport-C7YTA4I7.mjs → local-transport-RVD4FZGA.mjs} +5 -5
  119. package/dist/offline/index.js +51 -51
  120. package/dist/offline/index.mjs +6 -6
  121. package/dist/{request-66FGJIHB.mjs → request-4AWGPIGH.mjs} +2 -2
  122. package/dist/{request-6W6UGKJV.js → request-H5FZBZA2.js} +3 -3
  123. package/dist/{request-6W6UGKJV.js.map → request-H5FZBZA2.js.map} +1 -1
  124. package/dist/server/index.js +8 -8
  125. package/dist/server/index.mjs +7 -7
  126. package/dist/unified/JsonApiRequest.d.ts +5 -0
  127. package/dist/unified/JsonApiRequest.d.ts.map +1 -1
  128. package/dist/write-through-7I3LA2WN.mjs +15 -0
  129. package/dist/write-through-HE2QSOVI.js +15 -0
  130. package/dist/{write-through-BAPS24GL.js.map → write-through-HE2QSOVI.js.map} +1 -1
  131. package/package.json +1 -1
  132. package/src/client/JsonApiClient.ts +10 -0
  133. package/src/client/__tests__/request.credentials.spec.ts +103 -0
  134. package/src/client/request.ts +9 -0
  135. package/src/components/forms/EditorSheet.tsx +2 -2
  136. package/src/components/forms/__tests__/EditorSheet.spec.tsx +16 -0
  137. package/src/core/abstracts/AbstractService.ts +10 -0
  138. package/src/core/abstracts/ClientAbstractService.ts +10 -0
  139. package/src/core/index.ts +3 -0
  140. package/src/core/registry/ModuleRegistry.ts +4 -0
  141. package/src/features/discord-connection/data/__tests__/DiscordConnection.spec.ts +114 -0
  142. package/src/features/discord-connection/data/discord-connect-confirmation.interface.ts +16 -0
  143. package/src/features/discord-connection/data/discord-connect-confirmation.service.ts +21 -0
  144. package/src/features/discord-connection/data/discord-connect-confirmation.ts +46 -0
  145. package/src/features/discord-connection/data/discord-connect-intent.interface.ts +16 -0
  146. package/src/features/discord-connection/data/discord-connect-intent.service.ts +20 -0
  147. package/src/features/discord-connection/data/discord-connect-intent.ts +42 -0
  148. package/src/features/discord-connection/data/discord-connection.interface.ts +10 -0
  149. package/src/features/discord-connection/data/discord-connection.service.ts +17 -0
  150. package/src/features/discord-connection/data/discord-connection.ts +41 -0
  151. package/src/features/discord-connection/data/index.ts +9 -0
  152. package/src/features/discord-connection/discord-connection.module.ts +28 -0
  153. package/src/features/discord-connection/index.ts +2 -0
  154. package/src/features/oauth/components/OAuthClientDetail.tsx +1 -1
  155. package/src/features/user/contexts/UserContext.tsx +6 -2
  156. package/src/index.ts +1 -1
  157. package/src/unified/JsonApiRequest.ts +14 -0
  158. package/dist/JsonApiRequest-G4J2WTSU.js +0 -26
  159. package/dist/chunk-3CGU6WXY.js.map +0 -1
  160. package/dist/chunk-AUD6ZLBB.mjs.map +0 -1
  161. package/dist/chunk-GM4VZTWL.js.map +0 -1
  162. package/dist/chunk-JSHLBRFV.mjs.map +0 -1
  163. package/dist/chunk-KV4N7DEG.js.map +0 -1
  164. package/dist/chunk-M3N7LPHX.mjs.map +0 -1
  165. package/dist/chunk-MQ7TXALH.mjs.map +0 -1
  166. package/dist/chunk-OHHA5BBX.mjs.map +0 -1
  167. package/dist/chunk-WCEYT3GG.js.map +0 -1
  168. package/dist/chunk-ZB6OYIMB.js.map +0 -1
  169. package/dist/local-transport-4YDJ6FQG.js +0 -15
  170. package/dist/write-through-BAPS24GL.js +0 -15
  171. package/dist/write-through-IWQKXYHE.mjs +0 -15
  172. /package/dist/{BlockNoteEditor-66ZACETR.mjs.map → BlockNoteEditor-UO4ZRCUM.mjs.map} +0 -0
  173. /package/dist/{BlockNoteViewer-2UDFTXRS.mjs.map → BlockNoteViewer-2TJZ6JET.mjs.map} +0 -0
  174. /package/dist/{JsonApiRequest-IKKHWCPB.mjs.map → JsonApiRequest-ADLBJ5RX.mjs.map} +0 -0
  175. /package/dist/{chunk-4RA5PRXX.mjs.map → chunk-7OIWOPM5.mjs.map} +0 -0
  176. /package/dist/{chunk-G2UIQSNJ.mjs.map → chunk-TMV7TL7O.mjs.map} +0 -0
  177. /package/dist/{chunk-QIEF7HIL.mjs.map → chunk-VNUT2D27.mjs.map} +0 -0
  178. /package/dist/{chunk-KTGKW72G.mjs.map → chunk-W4ML6D2X.mjs.map} +0 -0
  179. /package/dist/{chunk-A3RB47X4.mjs.map → chunk-YTDGYI62.mjs.map} +0 -0
  180. /package/dist/{local-transport-C7YTA4I7.mjs.map → local-transport-RVD4FZGA.mjs.map} +0 -0
  181. /package/dist/{request-66FGJIHB.mjs.map → request-4AWGPIGH.mjs.map} +0 -0
  182. /package/dist/{write-through-IWQKXYHE.mjs.map → write-through-7I3LA2WN.mjs.map} +0 -0
@@ -93,6 +93,7 @@ export async function ClientJsonApiGet(params: {
93
93
  companyId?: string;
94
94
  language: string;
95
95
  baseUrl?: string;
96
+ credentials?: RequestCredentials;
96
97
  }): Promise<ApiResponseInterface> {
97
98
  runClientBootstrapper();
98
99
  const token = await getClientToken();
@@ -104,6 +105,7 @@ export async function ClientJsonApiGet(params: {
104
105
  companyId: params.companyId,
105
106
  language: params.language,
106
107
  additionalHeaders: _clientConfig?.additionalHeaders,
108
+ credentials: params.credentials,
107
109
  });
108
110
 
109
111
  return translateResponse({
@@ -132,6 +134,7 @@ export async function ClientJsonApiPost(params: {
132
134
  language: string;
133
135
  responseType?: ApiRequestDataTypeInterface;
134
136
  baseUrl?: string;
137
+ credentials?: RequestCredentials;
135
138
  }): Promise<ApiResponseInterface> {
136
139
  runClientBootstrapper();
137
140
  const token = await getClientToken();
@@ -152,6 +155,7 @@ export async function ClientJsonApiPost(params: {
152
155
  companyId: params.companyId,
153
156
  language: params.language,
154
157
  additionalHeaders: _clientConfig?.additionalHeaders,
158
+ credentials: params.credentials,
155
159
  });
156
160
 
157
161
  return translateResponse({
@@ -171,6 +175,7 @@ export async function ClientJsonApiPut(params: {
171
175
  language: string;
172
176
  responseType?: ApiRequestDataTypeInterface;
173
177
  baseUrl?: string;
178
+ credentials?: RequestCredentials;
174
179
  }): Promise<ApiResponseInterface> {
175
180
  runClientBootstrapper();
176
181
  const token = await getClientToken();
@@ -191,6 +196,7 @@ export async function ClientJsonApiPut(params: {
191
196
  companyId: params.companyId,
192
197
  language: params.language,
193
198
  additionalHeaders: _clientConfig?.additionalHeaders,
199
+ credentials: params.credentials,
194
200
  });
195
201
 
196
202
  return translateResponse({
@@ -211,6 +217,7 @@ export async function ClientJsonApiPatch(params: {
211
217
  responseType?: ApiRequestDataTypeInterface;
212
218
  language: string;
213
219
  baseUrl?: string;
220
+ credentials?: RequestCredentials;
214
221
  }): Promise<ApiResponseInterface> {
215
222
  runClientBootstrapper();
216
223
  const token = await getClientToken();
@@ -231,6 +238,7 @@ export async function ClientJsonApiPatch(params: {
231
238
  companyId: params.companyId,
232
239
  language: params.language,
233
240
  additionalHeaders: _clientConfig?.additionalHeaders,
241
+ credentials: params.credentials,
234
242
  });
235
243
 
236
244
  return translateResponse({
@@ -248,6 +256,7 @@ export async function ClientJsonApiDelete(params: {
248
256
  language: string;
249
257
  responseType?: ApiRequestDataTypeInterface;
250
258
  baseUrl?: string;
259
+ credentials?: RequestCredentials;
251
260
  }): Promise<ApiResponseInterface> {
252
261
  runClientBootstrapper();
253
262
  const token = await getClientToken();
@@ -259,6 +268,7 @@ export async function ClientJsonApiDelete(params: {
259
268
  companyId: params.companyId,
260
269
  language: params.language,
261
270
  additionalHeaders: _clientConfig?.additionalHeaders,
271
+ credentials: params.credentials,
262
272
  });
263
273
 
264
274
  return translateResponse({
@@ -0,0 +1,103 @@
1
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { AbstractService, HttpMethod } from "../../core/abstracts/AbstractService";
3
+ import { ClientAbstractService, ClientHttpMethod } from "../../core/abstracts/ClientAbstractService";
4
+ import { ApiRequestDataTypeInterface } from "../../core/interfaces/ApiRequestDataTypeInterface";
5
+ import { ModuleRegistry } from "../../core/registry/ModuleRegistry";
6
+ import { DiscordConnectIntent } from "../../features/discord-connection/data/discord-connect-intent";
7
+ import { directFetch } from "../request";
8
+
9
+ const TYPE = { name: "tests" } as any;
10
+ const ENDPOINT = "http://api.test/tests";
11
+ // Built by a model method, as references/frontend/03-services.md allows with overridesJsonApiCreation.
12
+ const body = () => new DiscordConnectIntent().createJsonApi({ id: "t-1", returnPath: "/" });
13
+
14
+ // createJsonApi() resolves Modules.X through the real ModuleRegistry,
15
+ // which is shared across test files in a worker: register only when absent.
16
+ const registerIfAbsent = (key: string, module: ApiRequestDataTypeInterface) => {
17
+ try {
18
+ ModuleRegistry.get(key as any);
19
+ } catch {
20
+ ModuleRegistry.register(key, module);
21
+ }
22
+ };
23
+
24
+ beforeAll(() => {
25
+ registerIfAbsent("DiscordConnectIntent", { name: "discord-connect-intents", model: DiscordConnectIntent } as any);
26
+ });
27
+
28
+ class TestService extends AbstractService {
29
+ static post(credentials?: RequestCredentials) {
30
+ return this.callApi({
31
+ type: TYPE,
32
+ method: HttpMethod.POST,
33
+ endpoint: ENDPOINT,
34
+ input: body(),
35
+ overridesJsonApiCreation: true,
36
+ suppressGlobalError: true,
37
+ ...(credentials ? { credentials } : {}),
38
+ });
39
+ }
40
+ }
41
+
42
+ class TestClientService extends ClientAbstractService {
43
+ static post(credentials?: RequestCredentials) {
44
+ return this.callApi({
45
+ type: TYPE,
46
+ method: ClientHttpMethod.POST,
47
+ endpoint: ENDPOINT,
48
+ input: body(),
49
+ overridesJsonApiCreation: true,
50
+ ...(credentials ? { credentials } : {}),
51
+ });
52
+ }
53
+ }
54
+
55
+ const fetchOptions = (fetchMock: ReturnType<typeof vi.fn>): RequestInit => fetchMock.mock.calls[0][1] as RequestInit;
56
+
57
+ describe("callApi credentials", () => {
58
+ let fetchMock: ReturnType<typeof vi.fn>;
59
+
60
+ beforeEach(() => {
61
+ // A 500 keeps the response path out of the test: only the fetch options matter.
62
+ fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({}), { status: 500 }));
63
+ vi.stubGlobal("fetch", fetchMock);
64
+ });
65
+
66
+ afterEach(() => {
67
+ vi.unstubAllGlobals();
68
+ });
69
+
70
+ it("callApi passes credentials to fetch", async () => {
71
+ await TestService.post("include").catch(() => undefined);
72
+
73
+ expect(fetchMock).toHaveBeenCalledTimes(1);
74
+ expect(fetchOptions(fetchMock).credentials).toBe("include");
75
+ });
76
+
77
+ it("callApi without credentials leaves fetch options unchanged", async () => {
78
+ await TestService.post().catch(() => undefined);
79
+
80
+ expect(fetchMock).toHaveBeenCalledTimes(1);
81
+ expect(Object.keys(fetchOptions(fetchMock)).sort()).toEqual(["body", "headers", "method"]);
82
+ });
83
+
84
+ it("ClientAbstractService callApi passes credentials to fetch", async () => {
85
+ await TestClientService.post("include").catch(() => undefined);
86
+
87
+ expect(fetchOptions(fetchMock).credentials).toBe("include");
88
+ });
89
+
90
+ it("ClientAbstractService callApi without credentials leaves fetch options unchanged", async () => {
91
+ await TestClientService.post().catch(() => undefined);
92
+
93
+ expect(Object.keys(fetchOptions(fetchMock)).sort()).toEqual(["body", "headers", "method"]);
94
+ });
95
+
96
+ it("directFetch sets credentials only when given", async () => {
97
+ await directFetch({ method: "GET", url: ENDPOINT, language: "en", credentials: "include" });
98
+ await directFetch({ method: "GET", url: ENDPOINT, language: "en" });
99
+
100
+ expect((fetchMock.mock.calls[0][1] as RequestInit).credentials).toBe("include");
101
+ expect("credentials" in (fetchMock.mock.calls[1][1] as RequestInit)).toBe(false);
102
+ });
103
+ });
@@ -14,6 +14,11 @@ export interface DirectFetchParams {
14
14
  companyId?: string;
15
15
  language: string;
16
16
  additionalHeaders?: Record<string, string>;
17
+ /**
18
+ * Optional `fetch` credentials mode (e.g. "include" so the browser stores a cookie the API sets).
19
+ * Omitted => no `credentials` option is passed, unchanged behaviour.
20
+ */
21
+ credentials?: RequestCredentials;
17
22
  }
18
23
 
19
24
  /**
@@ -108,6 +113,10 @@ async function fetchFromApi(params: DirectFetchParams): Promise<ApiData> {
108
113
  options.body = requestBody;
109
114
  }
110
115
 
116
+ if (params.credentials) {
117
+ options.credentials = params.credentials;
118
+ }
119
+
111
120
  if (params.token) {
112
121
  options.headers = { ...options.headers, Authorization: `Bearer ${params.token}` };
113
122
  }
@@ -320,8 +320,8 @@ export function EditorSheet<T extends FieldValues>({
320
320
  render={<div />}
321
321
  nativeButton={false}
322
322
  size="sm"
323
- variant={isLabelled ? "outline" : "ghost"}
324
- className="text-muted-foreground"
323
+ variant={isLabelled ? "default" : "ghost"}
324
+ className={isLabelled ? undefined : "text-muted-foreground"}
325
325
  >
326
326
  <PencilIcon />
327
327
  {isLabelled && t("ui.buttons.edit")}
@@ -77,6 +77,22 @@ describe("EditorSheet", () => {
77
77
  expect(screen.getByText("Custom")).toBeTruthy();
78
78
  });
79
79
 
80
+ it("should render a labelled edit trigger as a primary button", () => {
81
+ render(<TestEditor isEdit={true} showLabel={true} />);
82
+ const trigger = document.querySelector('[data-help="editor.edit"]');
83
+ expect(trigger?.textContent).toContain("ui.buttons.edit");
84
+ expect(trigger?.className).toContain("bg-primary");
85
+ expect(trigger?.className).not.toContain("text-muted-foreground");
86
+ });
87
+
88
+ it("should render an unlabelled edit trigger as a muted glyph", () => {
89
+ render(<TestEditor isEdit={true} showLabel={false} />);
90
+ const trigger = document.querySelector('[data-help="editor.edit"]');
91
+ expect(trigger?.textContent).not.toContain("ui.buttons.edit");
92
+ expect(trigger?.className).not.toContain("bg-primary");
93
+ expect(trigger?.className).toContain("text-muted-foreground");
94
+ });
95
+
80
96
  it("should not render trigger when dialogOpen is controlled", () => {
81
97
  render(<TestEditor dialogOpen={false} />);
82
98
  expect(screen.queryByText("ui.buttons.create")).toBeNull();
@@ -143,6 +143,11 @@ export abstract class AbstractService {
143
143
  * fully backward compatible. Ignored when `endpoint` starts with "http" (passthrough).
144
144
  */
145
145
  baseUrl?: string;
146
+ /**
147
+ * Optional browser `fetch` credentials mode, e.g. "include" so the browser stores a
148
+ * cookie the API sets. Omitted => no `credentials` option is passed (unchanged).
149
+ */
150
+ credentials?: RequestCredentials;
146
151
  }): Promise<T> {
147
152
  // Dynamic import to avoid bundling issues
148
153
  const { JsonApiGet, JsonApiPost, JsonApiPut, JsonApiPatch, JsonApiDelete } =
@@ -168,6 +173,7 @@ export abstract class AbstractService {
168
173
  language: language,
169
174
  token: params.token,
170
175
  baseUrl: params.baseUrl,
176
+ credentials: params.credentials,
171
177
  });
172
178
  break;
173
179
  case HttpMethod.POST:
@@ -182,6 +188,7 @@ export abstract class AbstractService {
182
188
  files: params.files,
183
189
  token: params.token,
184
190
  baseUrl: params.baseUrl,
191
+ credentials: params.credentials,
185
192
  });
186
193
  break;
187
194
  case HttpMethod.PUT:
@@ -195,6 +202,7 @@ export abstract class AbstractService {
195
202
  files: params.files,
196
203
  token: params.token,
197
204
  baseUrl: params.baseUrl,
205
+ credentials: params.credentials,
198
206
  });
199
207
  break;
200
208
  case HttpMethod.PATCH:
@@ -209,6 +217,7 @@ export abstract class AbstractService {
209
217
  files: params.files,
210
218
  token: params.token,
211
219
  baseUrl: params.baseUrl,
220
+ credentials: params.credentials,
212
221
  });
213
222
  break;
214
223
  case HttpMethod.DELETE:
@@ -220,6 +229,7 @@ export abstract class AbstractService {
220
229
  responseType: params.responseType,
221
230
  token: params.token,
222
231
  baseUrl: params.baseUrl,
232
+ credentials: params.credentials,
223
233
  });
224
234
  break;
225
235
  default:
@@ -148,6 +148,11 @@ export abstract class ClientAbstractService {
148
148
  * Omitted => existing global `NEXT_PUBLIC_API_URL` resolution, unchanged.
149
149
  */
150
150
  baseUrl?: string;
151
+ /**
152
+ * Optional browser `fetch` credentials mode, e.g. "include" so the browser stores a
153
+ * cookie the API sets. Omitted => no `credentials` option is passed (unchanged).
154
+ */
155
+ credentials?: RequestCredentials;
151
156
  }): Promise<T> {
152
157
  let apiResponse: ApiResponseInterface;
153
158
 
@@ -162,6 +167,7 @@ export abstract class ClientAbstractService {
162
167
  companyId: params.companyId,
163
168
  language: language,
164
169
  baseUrl: params.baseUrl,
170
+ credentials: params.credentials,
165
171
  });
166
172
  break;
167
173
  case ClientHttpMethod.POST:
@@ -175,6 +181,7 @@ export abstract class ClientAbstractService {
175
181
  responseType: params.responseType,
176
182
  files: params.files,
177
183
  baseUrl: params.baseUrl,
184
+ credentials: params.credentials,
178
185
  });
179
186
  break;
180
187
  case ClientHttpMethod.PUT:
@@ -187,6 +194,7 @@ export abstract class ClientAbstractService {
187
194
  responseType: params.responseType,
188
195
  files: params.files,
189
196
  baseUrl: params.baseUrl,
197
+ credentials: params.credentials,
190
198
  });
191
199
  break;
192
200
  case ClientHttpMethod.PATCH:
@@ -200,6 +208,7 @@ export abstract class ClientAbstractService {
200
208
  responseType: params.responseType,
201
209
  files: params.files,
202
210
  baseUrl: params.baseUrl,
211
+ credentials: params.credentials,
203
212
  });
204
213
  break;
205
214
  case ClientHttpMethod.DELETE:
@@ -210,6 +219,7 @@ export abstract class ClientAbstractService {
210
219
  language: language,
211
220
  responseType: params.responseType,
212
221
  baseUrl: params.baseUrl,
222
+ credentials: params.credentials,
213
223
  });
214
224
  break;
215
225
  default:
package/src/core/index.ts CHANGED
@@ -35,6 +35,9 @@ export * from "../permissions";
35
35
  export * from "../features/account-deletion/account-deletion.module";
36
36
  export * from "../features/account-deletion/data";
37
37
  export * from "../features/account-deletion/utils";
38
+ // Discord connect (opt-in): data and modules. No components ship from the library.
39
+ export * from "../features/discord-connection/discord-connection.module";
40
+ export * from "../features/discord-connection/data";
38
41
  export * from "../features/auth/auth.module";
39
42
  export * from "../features/auth/totp-authenticator.module";
40
43
  export * from "../features/auth/totp-setup.module";
@@ -109,6 +109,10 @@ export interface FoundationModuleDefinitions {
109
109
  AnalyticsPageView: ModuleWithPermissions;
110
110
  // Account deletion (opt-in: registered only by apps that enable it)
111
111
  AccountDeletion: ModuleWithPermissions;
112
+ // Discord connect (registered by apps that offer it)
113
+ DiscordConnection: ModuleWithPermissions;
114
+ DiscordConnectIntent: ModuleWithPermissions;
115
+ DiscordConnectConfirmation: ModuleWithPermissions;
112
116
  // Offline mode (opt-in: registered by apps that spread offlineModules)
113
117
  OfflineChangeSet: ModuleWithPermissions;
114
118
  RootSync: ModuleWithPermissions;
@@ -0,0 +1,114 @@
1
+ import { beforeAll, describe, expect, it } from "vitest";
2
+ import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
3
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
4
+ import { DiscordConnectConfirmation } from "../discord-connect-confirmation";
5
+ import { DiscordConnectIntent } from "../discord-connect-intent";
6
+ import { DiscordConnection } from "../discord-connection";
7
+
8
+ // createJsonApi() resolves Modules.X through the real ModuleRegistry,
9
+ // which is shared across test files in a worker: register only when absent.
10
+ const registerIfAbsent = (key: string, module: ApiRequestDataTypeInterface) => {
11
+ try {
12
+ ModuleRegistry.get(key as any);
13
+ } catch {
14
+ ModuleRegistry.register(key, module);
15
+ }
16
+ };
17
+
18
+ beforeAll(() => {
19
+ registerIfAbsent("DiscordConnection", { name: "discord-connections", model: DiscordConnection } as any);
20
+ registerIfAbsent("DiscordConnectIntent", { name: "discord-connect-intents", model: DiscordConnectIntent } as any);
21
+ registerIfAbsent("DiscordConnectConfirmation", {
22
+ name: "discord-connect-confirmations",
23
+ model: DiscordConnectConfirmation,
24
+ } as any);
25
+ });
26
+
27
+ describe("DiscordConnection", () => {
28
+ it("rehydrates connected and discordName", () => {
29
+ const model = new DiscordConnection().rehydrate({
30
+ jsonApi: { type: "discord-connections", id: "u-1", attributes: { connected: true, discordName: "carlo" } },
31
+ included: [],
32
+ } as any);
33
+
34
+ expect(model.id).toBe("u-1");
35
+ expect(model.connected).toBe(true);
36
+ expect(model.discordName).toBe("carlo");
37
+ });
38
+
39
+ it("defaults connected to false and discordName to undefined", () => {
40
+ const model = new DiscordConnection().rehydrate({
41
+ jsonApi: { type: "discord-connections", id: "u-1", attributes: {} },
42
+ included: [],
43
+ } as any);
44
+
45
+ expect(model.connected).toBe(false);
46
+ expect(model.discordName).toBeUndefined();
47
+ });
48
+
49
+ it("DiscordConnection createJsonApi has the canonical shape", () => {
50
+ expect(new DiscordConnection().createJsonApi({ id: "u-1" })).toEqual({
51
+ data: { type: "discord-connections", id: "u-1", attributes: {}, relationships: {} },
52
+ included: [],
53
+ });
54
+ });
55
+ });
56
+
57
+ describe("DiscordConnectIntent", () => {
58
+ it("rehydrates the intent returnPath and authorizeUrl", () => {
59
+ const model = new DiscordConnectIntent().rehydrate({
60
+ jsonApi: {
61
+ type: "discord-connect-intents",
62
+ id: "i-1",
63
+ attributes: {
64
+ returnPath: "/settings",
65
+ authorizeUrl: "http://api.test/discord-connect-intents/i-1/authorize",
66
+ },
67
+ },
68
+ included: [],
69
+ } as any);
70
+
71
+ expect(model.returnPath).toBe("/settings");
72
+ expect(model.authorizeUrl).toBe("http://api.test/discord-connect-intents/i-1/authorize");
73
+ });
74
+
75
+ it("DiscordConnectIntent createJsonApi sends the returnPath", () => {
76
+ expect(new DiscordConnectIntent().createJsonApi({ id: "i-1", returnPath: "/settings" })).toEqual({
77
+ data: { type: "discord-connect-intents", id: "i-1", attributes: { returnPath: "/settings" }, relationships: {} },
78
+ included: [],
79
+ });
80
+ });
81
+ });
82
+
83
+ describe("DiscordConnectConfirmation", () => {
84
+ it("DiscordConnectConfirmation createJsonApi identifies the token", () => {
85
+ expect(new DiscordConnectConfirmation().createJsonApi({ id: "t-1" })).toEqual({
86
+ data: { type: "discord-connect-confirmations", id: "t-1", attributes: {}, relationships: {} },
87
+ included: [],
88
+ });
89
+ });
90
+
91
+ it("rehydrates result and discordName", () => {
92
+ const model = new DiscordConnectConfirmation().rehydrate({
93
+ jsonApi: {
94
+ type: "discord-connect-confirmations",
95
+ id: "t-1",
96
+ attributes: { result: "connected", discordName: "carlo" },
97
+ },
98
+ included: [],
99
+ } as any);
100
+
101
+ expect(model.result).toBe("connected");
102
+ expect(model.discordName).toBe("carlo");
103
+ });
104
+
105
+ it("defaults a missing result to failed", () => {
106
+ const model = new DiscordConnectConfirmation().rehydrate({
107
+ jsonApi: { type: "discord-connect-confirmations", id: "t-1", attributes: {} },
108
+ included: [],
109
+ } as any);
110
+
111
+ expect(model.result).toBe("failed");
112
+ expect(model.discordName).toBeUndefined();
113
+ });
114
+ });
@@ -0,0 +1,16 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export type DiscordConnectResult = "connected" | "taken" | "already" | "failed";
4
+
5
+ /**
6
+ * The outcome of confirming a Connect Discord return. `result` defaults to "failed".
7
+ */
8
+ export interface DiscordConnectConfirmationInterface extends ApiDataInterface {
9
+ get result(): DiscordConnectResult;
10
+ get discordName(): string | undefined;
11
+ }
12
+
13
+ export type DiscordConnectConfirmationInput = {
14
+ /** The confirmation token from the `discordConnectConfirm` return parameter. */
15
+ id: string;
16
+ };
@@ -0,0 +1,21 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import {
3
+ DiscordConnectConfirmationInput,
4
+ DiscordConnectConfirmationInterface,
5
+ } from "./discord-connect-confirmation.interface";
6
+
7
+ export class DiscordConnectConfirmationService extends AbstractService {
8
+ /**
9
+ * POST the confirmation token from the Discord return address. Links Discord
10
+ * only when the signed-in user created the intent; the result says what happened.
11
+ */
12
+ static async create(params: DiscordConnectConfirmationInput): Promise<DiscordConnectConfirmationInterface> {
13
+ return this.callApi<DiscordConnectConfirmationInterface>({
14
+ type: Modules.DiscordConnectConfirmation,
15
+ method: HttpMethod.POST,
16
+ endpoint: new EndpointCreator({ endpoint: Modules.DiscordConnectConfirmation }).generate(),
17
+ input: params,
18
+ suppressGlobalError: true,
19
+ });
20
+ }
21
+ }
@@ -0,0 +1,46 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import {
3
+ DiscordConnectConfirmationInput,
4
+ DiscordConnectConfirmationInterface,
5
+ DiscordConnectResult,
6
+ } from "./discord-connect-confirmation.interface";
7
+
8
+ const RESULTS: readonly DiscordConnectResult[] = ["connected", "taken", "already", "failed"];
9
+
10
+ export class DiscordConnectConfirmation extends AbstractApiData implements DiscordConnectConfirmationInterface {
11
+ private _result: DiscordConnectResult = "failed";
12
+ private _discordName?: string;
13
+
14
+ get result(): DiscordConnectResult {
15
+ return this._result;
16
+ }
17
+
18
+ get discordName(): string | undefined {
19
+ return this._discordName;
20
+ }
21
+
22
+ rehydrate(data: JsonApiHydratedDataInterface): this {
23
+ super.rehydrate(data);
24
+
25
+ const attributes = data.jsonApi.attributes ?? {};
26
+
27
+ this._result = RESULTS.includes(attributes.result) ? attributes.result : "failed";
28
+ this._discordName = attributes.discordName ?? undefined;
29
+
30
+ return this;
31
+ }
32
+
33
+ createJsonApi(data: DiscordConnectConfirmationInput) {
34
+ const response: any = {
35
+ data: {
36
+ type: Modules.DiscordConnectConfirmation.name,
37
+ id: data.id,
38
+ attributes: {},
39
+ relationships: {},
40
+ },
41
+ included: [],
42
+ };
43
+
44
+ return response;
45
+ }
46
+ }
@@ -0,0 +1,16 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ /**
4
+ * A one-time Connect Discord intent. Its id is a UUID the browser generates.
5
+ */
6
+ export interface DiscordConnectIntentInterface extends ApiDataInterface {
7
+ get returnPath(): string;
8
+ get authorizeUrl(): string | undefined;
9
+ }
10
+
11
+ export type DiscordConnectIntentInput = {
12
+ /** A UUID generated by the browser (uuid `v4()`). */
13
+ id: string;
14
+ /** Where the user lands after Discord: `/`, `/settings` or `/settings?section=<id>`. */
15
+ returnPath: string;
16
+ };
@@ -0,0 +1,20 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { DiscordConnectIntentInput, DiscordConnectIntentInterface } from "./discord-connect-intent.interface";
3
+
4
+ export class DiscordConnectIntentService extends AbstractService {
5
+ /**
6
+ * POST a Connect Discord intent for the signed-in user. Sent with
7
+ * `credentials: "include"` so the browser stores the API's HttpOnly
8
+ * `discord_connect` cookie that binds the intent to this browser.
9
+ */
10
+ static async create(params: DiscordConnectIntentInput): Promise<DiscordConnectIntentInterface> {
11
+ return this.callApi<DiscordConnectIntentInterface>({
12
+ type: Modules.DiscordConnectIntent,
13
+ method: HttpMethod.POST,
14
+ endpoint: new EndpointCreator({ endpoint: Modules.DiscordConnectIntent }).generate(),
15
+ input: params,
16
+ credentials: "include",
17
+ suppressGlobalError: true,
18
+ });
19
+ }
20
+ }
@@ -0,0 +1,42 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { DiscordConnectIntentInput, DiscordConnectIntentInterface } from "./discord-connect-intent.interface";
3
+
4
+ export class DiscordConnectIntent extends AbstractApiData implements DiscordConnectIntentInterface {
5
+ private _returnPath?: string;
6
+ private _authorizeUrl?: string;
7
+
8
+ get returnPath(): string {
9
+ return this._returnPath ?? "/";
10
+ }
11
+
12
+ get authorizeUrl(): string | undefined {
13
+ return this._authorizeUrl;
14
+ }
15
+
16
+ rehydrate(data: JsonApiHydratedDataInterface): this {
17
+ super.rehydrate(data);
18
+
19
+ const attributes = data.jsonApi.attributes ?? {};
20
+
21
+ this._returnPath = attributes.returnPath;
22
+ this._authorizeUrl = attributes.authorizeUrl ?? undefined;
23
+
24
+ return this;
25
+ }
26
+
27
+ createJsonApi(data: DiscordConnectIntentInput) {
28
+ const response: any = {
29
+ data: {
30
+ type: Modules.DiscordConnectIntent.name,
31
+ id: data.id,
32
+ attributes: {
33
+ returnPath: data.returnPath,
34
+ },
35
+ relationships: {},
36
+ },
37
+ included: [],
38
+ };
39
+
40
+ return response;
41
+ }
42
+ }
@@ -0,0 +1,10 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ /**
4
+ * Whether the signed-in user has a Discord account attached. Not a stored entity:
5
+ * the API builds it for one user; its id is that user's id.
6
+ */
7
+ export interface DiscordConnectionInterface extends ApiDataInterface {
8
+ get connected(): boolean;
9
+ get discordName(): string | undefined;
10
+ }
@@ -0,0 +1,17 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { DiscordConnectionInterface } from "./discord-connection.interface";
3
+
4
+ export class DiscordConnectionService extends AbstractService {
5
+ /**
6
+ * GET whether the user has Discord attached. `params.id` must be the signed-in
7
+ * user's id; any other id answers 404.
8
+ */
9
+ static async findById(params: { id: string }): Promise<DiscordConnectionInterface> {
10
+ return this.callApi<DiscordConnectionInterface>({
11
+ type: Modules.DiscordConnection,
12
+ method: HttpMethod.GET,
13
+ endpoint: new EndpointCreator({ endpoint: Modules.DiscordConnection, id: params.id }).generate(),
14
+ suppressGlobalError: true,
15
+ });
16
+ }
17
+ }