@godxjp/sdk 0.3.0 → 0.5.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.
package/README.md CHANGED
@@ -99,11 +99,20 @@ export const apiClient = createClient({
99
99
  // On a 401, before `signInUrl`: resolve `true` and the request is sent once
100
100
  // more; resolve `false` and the 401 takes its usual course. Never more than
101
101
  // one retry per request, and 401s that arrive together share one call.
102
- onUnauthorized: () => tokens.refresh(),
102
+ // `ctx` is the request that was answered 401 — method, URL and the headers
103
+ // it was sent with — so a login that failed is not mistaken for an expired
104
+ // token.
105
+ onUnauthorized: ({ headers }) =>
106
+ headers.Authorization === undefined
107
+ ? Promise.resolve(false)
108
+ : tokens.refresh(),
109
+ // A request that never reached the server: `ApiError` with status 0 by
110
+ // default ("wrap"); "throw" rethrows fetch's own `TypeError`.
111
+ networkErrors: "throw",
103
112
  });
104
113
  ```
105
114
 
106
- A request body is always JSON text or `FormData`, never a stream, so it can be sent a second time. A request cancelled while the token was refreshed stays an `AbortError`.
115
+ A request body is always JSON text or `FormData`, never a stream, so it can be sent a second time. A request cancelled while the token was refreshed stays an `AbortError`, whatever `networkErrors` says.
107
116
 
108
117
  ### Downloading a file
109
118
 
@@ -129,10 +138,23 @@ createClient({
129
138
  });
130
139
  ```
131
140
 
141
+ Everything else the server sent is on `ApiError.body` — the parsed JSON, or `undefined` when there was none, it was not JSON, or the server was never reached. Read an `error` code, a `message` that is not a string, or every message per field from there:
142
+
143
+ ```ts
144
+ if (error instanceof ApiError) {
145
+ const { error: code, errors } = (error.body ?? {}) as {
146
+ error?: string;
147
+ errors?: Record<string, string[]>;
148
+ };
149
+ }
150
+ ```
151
+
132
152
  An unreachable server (`status: 0`) keeps its built-in text, `Could not reach the server. Check your connection and try again.`, unless `messages(0)` answers one.
133
153
 
134
154
  **Changed in 0.3.0.** Before, a 419 always carried `Your session has expired. Reload the page and try again.` and a response without a message fell back to `statusText`. Both are now the server's message or empty. To keep the old 419 text, pass `messages: (status) => status === 419 ? "Your session has expired. Reload the page and try again." : undefined`.
135
155
 
156
+ **Changed in 0.4.0.** Additions only; a client configured for 0.3.0 sends and throws exactly what it did. `ApiError` gains `body` (also on a `download()` failure). `onUnauthorized` now receives the request that was answered 401 (`{ method, url, headers }`); a function that takes no argument still works. `networkErrors: "throw"` rethrows `fetch`'s own error instead of `ApiError(0)` — including a response body that breaks off mid-way.
157
+
136
158
  ## `./realtime`
137
159
 
138
160
  ```ts
@@ -149,6 +171,27 @@ import {
149
171
 
150
172
  So nothing from the transport appears in this API — no Echo type, no Pusher connection state, no channel object. A consumer able to name the underlying client has already defeated the point.
151
173
 
174
+ ### Two transports
175
+
176
+ ```ts
177
+ // The product's own Reverb, as before (a config without `driver`):
178
+ configureRealtime({ key, host, port, scheme });
179
+
180
+ // The platform's Centrifugo, on the socket the GoDX Dock already holds (godx-jp/id#2019):
181
+ configureRealtime({
182
+ driver: "godx-dock",
183
+ channelPrefix: `private-svc.${clientId}.`,
184
+ authEndpoint: "/broadcasting/auth",
185
+ });
186
+ ```
187
+
188
+ With `godx-dock` the server publishes through php-sdk's `godx` broadcast driver, and this package
189
+ listens through `window.GodxDock.realtime()`. The Dock lends only a narrow `subscribe` for
190
+ `private-svc.*` channels, never its client. Channel and event names stay the product's own
191
+ (`issue.7`, `.issue.changed`). The subscription token comes from `authEndpoint`, so your
192
+ `routes/channels.php` still decides. A page with no Dock, no session or no Centrifugo reports
193
+ `unavailable`, and your polling fallback takes over.
194
+
152
195
  ### Three words, not six
153
196
 
154
197
  ```ts
@@ -13,7 +13,14 @@ declare class ApiError extends Error {
13
13
  readonly status: number;
14
14
  /** Laravel's `{errors: {field: string[]}}`, first message per key; `{}` when absent. */
15
15
  readonly errors: Record<string, string>;
16
- constructor(status: number, message: string, errors?: Record<string, string>);
16
+ /**
17
+ * The error response's body, parsed as JSON — everything `message` and
18
+ * `errors` leave out: an `error` code, a `message` that is not a string,
19
+ * every message per field. `undefined` when there was no body, it was not
20
+ * JSON, or the server was never reached.
21
+ */
22
+ readonly body: unknown;
23
+ constructor(status: number, message: string, errors?: Record<string, string>, body?: unknown);
17
24
  }
18
25
 
19
26
  /**
@@ -57,6 +64,14 @@ type DownloadResult = {
57
64
  headers: Headers;
58
65
  status: number;
59
66
  };
67
+ /** The request a 401 answered, as `onUnauthorized` receives it. */
68
+ type UnauthorizedContext = {
69
+ method: string;
70
+ /** The full URL, `baseUrl` and query string included. */
71
+ url: string;
72
+ /** Exactly the headers the request was sent with. */
73
+ headers: Record<string, string>;
74
+ };
60
75
  type ClientConfig = {
61
76
  /** Prefixed to every path, e.g. `/api/app/v1`. */
62
77
  baseUrl: string;
@@ -81,9 +96,22 @@ type ClientConfig = {
81
96
  * Called on a 401 before `signInUrl`. Resolve `true` (a token was refreshed,
82
97
  * say) and the request is sent once more, headers read afresh; resolve
83
98
  * `false` and the 401 takes its usual course. A request is retried at most
84
- * once, and 401s that arrive together share one call.
99
+ * once, and 401s that arrive together share one call — made with the
100
+ * context of the request that started it.
101
+ *
102
+ * `ctx` describes the request that was answered 401, with the headers it
103
+ * was actually sent with — so a host can refresh only when that request
104
+ * carried a token.
105
+ */
106
+ onUnauthorized?: (ctx: UnauthorizedContext) => Promise<boolean>;
107
+ /**
108
+ * What a request that never reached the server throws. `wrap` (the
109
+ * default) turns it into `ApiError` with status `0`; `throw` rethrows what
110
+ * `fetch` threw (a `TypeError`), for a host that tells a network failure
111
+ * from an HTTP one by exactly that. A cancelled request is an `AbortError`
112
+ * either way.
85
113
  */
86
- onUnauthorized?: () => Promise<boolean>;
114
+ networkErrors?: "wrap" | "throw";
87
115
  /**
88
116
  * The message for an `ApiError` whose response carried none — the client
89
117
  * has no language of its own. Unset, or answering `undefined`, leaves
@@ -115,4 +143,4 @@ declare function queryString(query: ApiQuery | undefined): string;
115
143
  declare function flattenErrors(errors: unknown): Record<string, string>;
116
144
  declare function createClient(config: ClientConfig): ApiClient;
117
145
 
118
- export { type ApiClient, ApiError, type ApiQuery, type ClientConfig, type DownloadOptions, type DownloadResult, type GetOptions, type WriteOptions, createClient, flattenErrors, queryString };
146
+ export { type ApiClient, ApiError, type ApiQuery, type ClientConfig, type DownloadOptions, type DownloadResult, type GetOptions, type UnauthorizedContext, type WriteOptions, createClient, flattenErrors, queryString };
@@ -3,11 +3,19 @@ var ApiError = class extends Error {
3
3
  status;
4
4
  /** Laravel's `{errors: {field: string[]}}`, first message per key; `{}` when absent. */
5
5
  errors;
6
- constructor(status, message, errors = {}) {
6
+ /**
7
+ * The error response's body, parsed as JSON — everything `message` and
8
+ * `errors` leave out: an `error` code, a `message` that is not a string,
9
+ * every message per field. `undefined` when there was no body, it was not
10
+ * JSON, or the server was never reached.
11
+ */
12
+ body;
13
+ constructor(status, message, errors = {}, body) {
7
14
  super(message);
8
15
  this.name = "ApiError";
9
16
  this.status = status;
10
17
  this.errors = errors;
18
+ this.body = body;
11
19
  }
12
20
  };
13
21
 
@@ -112,7 +120,10 @@ function send(url, init, onUploadProgress) {
112
120
  xhr.send(init.body ?? null);
113
121
  });
114
122
  }
115
- function unreachable(config) {
123
+ function unreachable(config, error) {
124
+ if (config.networkErrors === "throw") {
125
+ return error;
126
+ }
116
127
  return new ApiError(0, config.messages?.(0) ?? UNREACHABLE);
117
128
  }
118
129
  async function read(response, as, config, signal) {
@@ -122,7 +133,7 @@ async function read(response, as, config, signal) {
122
133
  if (signal?.aborted) {
123
134
  throw error;
124
135
  }
125
- throw unreachable(config);
136
+ throw unreachable(config, error);
126
137
  }
127
138
  }
128
139
  async function exchange({ config, unauthorized }, method, path, body, { signal, query, onUploadProgress }, retried = false) {
@@ -142,10 +153,11 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
142
153
  payload = JSON.stringify(body);
143
154
  }
144
155
  Object.assign(headers, await config.headers?.());
156
+ const url = `${config.baseUrl}${path}${queryString(query)}`;
145
157
  let response;
146
158
  try {
147
159
  response = await send(
148
- `${config.baseUrl}${path}${queryString(query)}`,
160
+ url,
149
161
  {
150
162
  method,
151
163
  headers,
@@ -159,12 +171,12 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
159
171
  if (signal?.aborted) {
160
172
  throw error;
161
173
  }
162
- throw unreachable(config);
174
+ throw unreachable(config, error);
163
175
  }
164
176
  if (response.status >= 200 && response.status < 300) {
165
177
  return response;
166
178
  }
167
- if (response.status === 401 && !retried && config.onUnauthorized !== void 0 && await unauthorized()) {
179
+ if (response.status === 401 && !retried && config.onUnauthorized !== void 0 && await unauthorized({ method, url, headers: { ...headers } })) {
168
180
  return exchange(
169
181
  { config, unauthorized },
170
182
  method,
@@ -187,7 +199,8 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
187
199
  throw new ApiError(
188
200
  response.status,
189
201
  typeof bag.message === "string" && bag.message !== "" ? bag.message : config.messages?.(response.status) ?? "",
190
- flattenErrors(bag.errors)
202
+ flattenErrors(bag.errors),
203
+ json
191
204
  );
192
205
  }
193
206
  async function request(client, method, path, body, options) {
@@ -206,7 +219,7 @@ function createClient(config) {
206
219
  let refreshing;
207
220
  const client = {
208
221
  config,
209
- unauthorized: () => refreshing ??= (config.onUnauthorized?.() ?? Promise.resolve(false)).finally(() => {
222
+ unauthorized: (ctx) => refreshing ??= (config.onUnauthorized?.(ctx) ?? Promise.resolve(false)).finally(() => {
210
223
  refreshing = void 0;
211
224
  })
212
225
  };
@@ -22,8 +22,9 @@
22
22
  * because a screen has the same job in all three.
23
23
  */
24
24
  type RealtimeStatus = "connecting" | "connected" | "unavailable";
25
- /** What the host must hand over to reach its own server. */
26
- type RealtimeConfig = {
25
+ /** A product's own Reverb server — what every config meant before `driver` existed. */
26
+ type ReverbRealtimeConfig = {
27
+ driver?: "reverb";
27
28
  /** The app key the server issued. */
28
29
  key: string;
29
30
  host: string;
@@ -31,6 +32,19 @@ type RealtimeConfig = {
31
32
  /** `https` turns on TLS and the `wss` transport. */
32
33
  scheme: "http" | "https";
33
34
  };
35
+ /**
36
+ * The platform's Centrifugo, over the socket the GoDX Dock already holds (godx-jp/id#2019). The
37
+ * server side is php-sdk's `godx` broadcast driver.
38
+ */
39
+ type DockRealtimeConfig = {
40
+ driver: "godx-dock";
41
+ /** `private-svc.{client_id}.` — where the platform relay puts every channel of this service. */
42
+ channelPrefix: string;
43
+ /** The product's own channel authorization endpoint, usually `/broadcasting/auth`. */
44
+ authEndpoint: string;
45
+ };
46
+ /** What the host must hand over to reach its realtime server. */
47
+ type RealtimeConfig = ReverbRealtimeConfig | DockRealtimeConfig;
34
48
  /**
35
49
  * Map a transport's own vocabulary onto ours.
36
50
  *
@@ -39,44 +53,28 @@ type RealtimeConfig = {
39
53
  */
40
54
  declare function toRealtimeStatus(raw: string | null | undefined): RealtimeStatus;
41
55
 
42
- /**
43
- * The Echo/Pusher adapter — today's transport, and the only file in this
44
- * package that may import it.
45
- */
46
56
  /**
47
57
  * Wire the connection once, for the life of the tab.
48
58
  *
49
- * The host reads its own server's details wherever it keeps them (a meta tag,
50
- * an API call) and passes them in. The SDK does not guess: `import.meta.env`
51
- * belongs to whoever runs the bundler, and four products do not share one.
59
+ * The host reads its own server's details wherever it keeps them (a meta tag, an API call) and
60
+ * passes them in. A config without `driver` is the product's own Reverb, as before.
52
61
  */
53
62
  declare function configureRealtime(config: RealtimeConfig): void;
54
63
  /**
55
- * Whether `configureRealtime` ran.
56
- *
57
- * CONFIGURED IS NOT CONNECTED, and conflating them is a real bug this project
58
- * has already shipped once: a screen that reads "configured" as "live" keeps
59
- * its polling fallback switched off while the server is down, and freezes on
60
- * whatever it loaded with. Ask `useRealtimeStatus()` for liveness.
64
+ * Whether `configureRealtime` ran. CONFIGURED IS NOT CONNECTED: ask `useRealtimeStatus()` for
65
+ * liveness.
61
66
  */
62
67
  declare function isRealtimeConfigured(): boolean;
63
- /** Liveness, in this package's three words rather than the transport's six. */
68
+ /** Liveness, in this package's three words rather than the transport's own. */
64
69
  declare function useRealtimeStatus(): RealtimeStatus;
65
70
  /**
66
- * Listen to one named event on one channel.
67
- *
68
- * `event` is the name the server broadcasts, leading dot and all — the
69
- * transport's convention for "this is our own event, not a framework one".
71
+ * Listen to one named event on one private channel. `event` is the name the server broadcasts,
72
+ * leading dot and all.
70
73
  */
71
74
  declare function useChannelEvent<T = unknown>(channel: string, event: string, handler: (payload: T) => void, deps?: unknown[]): void;
72
75
  /**
73
- * Listen to a user's notification channel.
74
- *
75
- * SEPARATE FROM `useChannelEvent` ON PURPOSE. Laravel broadcasts every
76
- * notification under one framework event name, so subscribing by a custom name
77
- * succeeds and then never fires — a failure that presents as "connected but
78
- * nothing happens". Naming the two cases apart is what stops the next person
79
- * reaching for the wrong one.
76
+ * Listen to a user's notification channel. SEPARATE FROM `useChannelEvent` ON PURPOSE: Laravel
77
+ * broadcasts every notification under one framework event name.
80
78
  */
81
79
  declare function useUserNotification<T = unknown>(channel: string, handler: (payload: T) => void): void;
82
80
 
@@ -113,4 +111,4 @@ declare function platformChannel(resource: string, resourceId: string): ChannelN
113
111
  declare function parseChannelName(name: string): ParsedChannelName;
114
112
  declare function tryParseChannelName(name: string): ParsedChannelName | null;
115
113
 
116
- export { type ChannelName, ChannelNameError, type ParsedChannelName, type RealtimeConfig, type RealtimeStatus, configureRealtime, isRealtimeConfigured, organizationChannel, parseChannelName, platformChannel, toRealtimeStatus, tryParseChannelName, useChannelEvent, useRealtimeStatus, useUserNotification };
114
+ export { type ChannelName, ChannelNameError, type DockRealtimeConfig, type ParsedChannelName, type RealtimeConfig, type RealtimeStatus, type ReverbRealtimeConfig, configureRealtime, isRealtimeConfigured, organizationChannel, parseChannelName, platformChannel, toRealtimeStatus, tryParseChannelName, useChannelEvent, useRealtimeStatus, useUserNotification };
@@ -1,6 +1,128 @@
1
+ import { useSyncExternalStore, useRef, useEffect } from 'react';
1
2
  import { configureEcho, echoIsConfigured, useConnectionStatus, useEcho, useEchoNotification } from '@laravel/echo-react';
2
3
 
3
- // src/realtime/echo-adapter.ts
4
+ // src/realtime/dock-adapter.ts
5
+ var NOTIFICATION_EVENT = "Illuminate\\Notifications\\Events\\BroadcastNotificationCreated";
6
+ var config = null;
7
+ var dock = null;
8
+ function configureDockRealtime(next) {
9
+ config = next;
10
+ dock = null;
11
+ }
12
+ function isDockRealtimeConfigured() {
13
+ return config !== null;
14
+ }
15
+ function dockRealtime() {
16
+ dock ??= new Promise((resolve) => {
17
+ const found = () => window.GodxDock ?? null;
18
+ if (found() !== null || document.readyState === "complete") {
19
+ resolve(found());
20
+ } else {
21
+ window.addEventListener("load", () => resolve(found()), { once: true });
22
+ }
23
+ }).then((global) => global?.realtime === void 0 ? null : global.realtime());
24
+ return dock;
25
+ }
26
+ function readCookie(name) {
27
+ const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
28
+ return match?.[1] === void 0 ? null : decodeURIComponent(match[1]);
29
+ }
30
+ async function subscriptionToken(channel) {
31
+ const active = config;
32
+ if (active === null) {
33
+ throw new Error("realtime is not configured");
34
+ }
35
+ const xsrf = readCookie("XSRF-TOKEN");
36
+ const response = await fetch(active.authEndpoint, {
37
+ method: "POST",
38
+ credentials: "same-origin",
39
+ headers: {
40
+ Accept: "application/json",
41
+ "Content-Type": "application/json",
42
+ ...xsrf === null ? {} : { "X-XSRF-TOKEN": xsrf }
43
+ },
44
+ body: JSON.stringify({ channel_name: `private-${channel}` })
45
+ });
46
+ if (!response.ok) {
47
+ throw new Error(`channel authorization refused (${response.status})`);
48
+ }
49
+ const data = await response.json();
50
+ if (typeof data.token !== "string") {
51
+ throw new Error("channel authorization answered without a token");
52
+ }
53
+ return data.token;
54
+ }
55
+ function literal(event) {
56
+ return event.startsWith(".") ? event.slice(1) : event;
57
+ }
58
+ function useDockSubscription(channel, event, handler, deps) {
59
+ const latest = useRef(handler);
60
+ latest.current = handler;
61
+ useEffect(() => {
62
+ const active = config;
63
+ if (active === null) {
64
+ return;
65
+ }
66
+ let stop = null;
67
+ let cancelled = false;
68
+ const wanted = literal(event);
69
+ void dockRealtime().then((realtime) => {
70
+ if (realtime === null || cancelled) {
71
+ return;
72
+ }
73
+ stop = realtime.subscribe(`${active.channelPrefix}${channel}`, {
74
+ getToken: () => subscriptionToken(channel),
75
+ onPublication: (name, data) => {
76
+ if (name === wanted) {
77
+ latest.current(data);
78
+ }
79
+ }
80
+ });
81
+ });
82
+ return () => {
83
+ cancelled = true;
84
+ stop?.();
85
+ };
86
+ }, [channel, event, ...deps]);
87
+ }
88
+ function useDockChannelEvent(channel, event, handler, deps) {
89
+ useDockSubscription(channel, event, handler, deps);
90
+ }
91
+ function useDockUserNotification(channel, handler) {
92
+ useDockSubscription(channel, NOTIFICATION_EVENT, handler, []);
93
+ }
94
+ var status = "connecting";
95
+ var statusListeners = /* @__PURE__ */ new Set();
96
+ var watching = false;
97
+ function watchStatus() {
98
+ if (watching || config === null) {
99
+ return;
100
+ }
101
+ watching = true;
102
+ void dockRealtime().then((realtime) => {
103
+ const set = (next) => {
104
+ status = next;
105
+ statusListeners.forEach((listener) => listener());
106
+ };
107
+ if (realtime === null) {
108
+ set("unavailable");
109
+ return;
110
+ }
111
+ set(realtime.status());
112
+ realtime.onStatus(set);
113
+ });
114
+ }
115
+ function useDockRealtimeStatus() {
116
+ return useSyncExternalStore(
117
+ (listener) => {
118
+ statusListeners.add(listener);
119
+ watchStatus();
120
+ return () => statusListeners.delete(listener);
121
+ },
122
+ () => status,
123
+ () => "connecting"
124
+ );
125
+ }
4
126
 
5
127
  // src/realtime/types.ts
6
128
  function toRealtimeStatus(raw) {
@@ -14,30 +136,62 @@ function toRealtimeStatus(raw) {
14
136
  }
15
137
 
16
138
  // src/realtime/echo-adapter.ts
17
- function configureRealtime(config) {
139
+ function configureEchoRealtime(config2) {
18
140
  configureEcho({
19
141
  broadcaster: "reverb",
20
- key: config.key,
21
- wsHost: config.host,
22
- wsPort: config.port,
23
- wssPort: config.port,
24
- forceTLS: config.scheme === "https",
142
+ key: config2.key,
143
+ wsHost: config2.host,
144
+ wsPort: config2.port,
145
+ wssPort: config2.port,
146
+ forceTLS: config2.scheme === "https",
25
147
  enabledTransports: ["ws", "wss"]
26
148
  });
27
149
  }
28
- function isRealtimeConfigured() {
150
+ function isEchoRealtimeConfigured() {
29
151
  return echoIsConfigured();
30
152
  }
31
- function useRealtimeStatus() {
153
+ function useEchoRealtimeStatus() {
32
154
  return toRealtimeStatus(useConnectionStatus());
33
155
  }
34
- function useChannelEvent(channel, event, handler, deps = []) {
156
+ function useEchoChannelEvent(channel, event, handler, deps = []) {
35
157
  useEcho(channel, event, handler, deps);
36
158
  }
37
- function useUserNotification(channel, handler) {
159
+ function useEchoUserNotification(channel, handler) {
38
160
  useEchoNotification(channel, handler);
39
161
  }
40
162
 
163
+ // src/realtime/select.ts
164
+ var driver = "reverb";
165
+ function configureRealtime(config2) {
166
+ if (config2.driver === "godx-dock") {
167
+ driver = "godx-dock";
168
+ configureDockRealtime(config2);
169
+ return;
170
+ }
171
+ driver = "reverb";
172
+ configureEchoRealtime(config2);
173
+ }
174
+ function isRealtimeConfigured() {
175
+ return driver === "godx-dock" ? isDockRealtimeConfigured() : isEchoRealtimeConfigured();
176
+ }
177
+ function useRealtimeStatus() {
178
+ return driver === "godx-dock" ? useDockRealtimeStatus() : useEchoRealtimeStatus();
179
+ }
180
+ function useChannelEvent(channel, event, handler, deps = []) {
181
+ if (driver === "godx-dock") {
182
+ useDockChannelEvent(channel, event, handler, deps);
183
+ } else {
184
+ useEchoChannelEvent(channel, event, handler, deps);
185
+ }
186
+ }
187
+ function useUserNotification(channel, handler) {
188
+ if (driver === "godx-dock") {
189
+ useDockUserNotification(channel, handler);
190
+ } else {
191
+ useEchoUserNotification(channel, handler);
192
+ }
193
+ }
194
+
41
195
  // src/realtime/channel-name.ts
42
196
  var ChannelNameError = class extends Error {
43
197
  constructor(message) {
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@godxjp/sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "GoDX JavaScript SDK \u2014 shared frontend infrastructure for GoDX products.",
5
5
  "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/godx-jp/web-sdk.git"
9
+ },
10
+ "homepage": "https://github.com/godx-jp/web-sdk#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/godx-jp/web-sdk/issues"
13
+ },
6
14
  "type": "module",
7
15
  "sideEffects": false,
8
16
  "files": [
@@ -36,10 +44,12 @@
36
44
  "@laravel/echo-react": "^2.5.0",
37
45
  "@types/node": "^22.10.2",
38
46
  "@types/react": "^19.3.0",
47
+ "@types/react-dom": "^19.3.0",
39
48
  "jsdom": "^25.0.1",
40
49
  "laravel-echo": "^2.5.0",
41
50
  "pusher-js": "^8.6.0",
42
51
  "react": "^19.3.0",
52
+ "react-dom": "^19.3.0",
43
53
  "tsup": "^8.5.1",
44
54
  "typescript": "^5.7.2",
45
55
  "vitest": "^2.1.8"