@segmentstream-cloud/app 0.2.0 → 0.3.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 +192 -87
- package/dist/app.d.ts +35 -8
- package/dist/app.js +73 -8
- package/dist/config.d.ts +17 -1
- package/dist/config.js +29 -7
- package/dist/cookies.d.ts +16 -7
- package/dist/cookies.js +36 -47
- package/dist/data.d.ts +6 -18
- package/dist/data.js +17 -36
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +66 -0
- package/dist/handlers.js +43 -19
- package/dist/index.d.ts +18 -8
- package/dist/index.js +11 -6
- package/dist/me.d.ts +24 -0
- package/dist/me.js +33 -0
- package/dist/neon.d.ts +92 -0
- package/dist/neon.js +414 -0
- package/dist/ontology-transport.d.ts +23 -0
- package/dist/ontology-transport.js +95 -0
- package/dist/ontology.d.ts +217 -0
- package/dist/ontology.js +141 -0
- package/dist/permissions.d.ts +17 -0
- package/dist/permissions.js +33 -0
- package/dist/proxy.d.ts +2 -0
- package/dist/proxy.js +29 -13
- package/dist/return-to.d.ts +2 -0
- package/dist/return-to.js +24 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +35 -5
- package/dist/sign-in.d.ts +5 -1
- package/dist/sign-in.js +17 -4
- package/dist/tokens.d.ts +22 -3
- package/dist/ui/components.d.ts +27 -0
- package/dist/ui/components.js +63 -0
- package/dist/ui/hooks.d.ts +19 -0
- package/dist/ui/hooks.js +56 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +8 -0
- package/dist/ui/navigation.d.ts +5 -0
- package/dist/ui/navigation.js +8 -0
- package/dist/ui/sign-in.d.ts +19 -0
- package/dist/ui/sign-in.js +198 -0
- package/dist/ui/store.d.ts +61 -0
- package/dist/ui/store.js +155 -0
- package/dist/ui/styles.css +249 -0
- package/package.json +24 -7
package/dist/ui/store.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createAuthClient } from "@neondatabase/auth/next";
|
|
3
|
+
import { NOT_INVITED, SegmentStreamApiError, apiErrorFrom, readJson } from "../errors.js";
|
|
4
|
+
import { ME_PATH, parseMe } from "../me.js";
|
|
5
|
+
/** The Neon Auth API the app serves from `app/src/app/api/auth/[...path]/route.ts`. */
|
|
6
|
+
export const AUTH_API_PATH = "/api/auth";
|
|
7
|
+
/** The app's sign-in page. */
|
|
8
|
+
export const SIGN_IN_PAGE = "/sign-in";
|
|
9
|
+
export { NOT_INVITED };
|
|
10
|
+
export const LOADING_USER = { status: "loading", user: null, error: null };
|
|
11
|
+
export const SIGNED_OUT_USER = { status: "signed-out", user: null, error: null };
|
|
12
|
+
const SERVER_SNAPSHOT = { session: { status: "loading" }, users: new Map() };
|
|
13
|
+
let snapshot = SERVER_SNAPSHOT;
|
|
14
|
+
let sessionLoad = null;
|
|
15
|
+
const loadingUsers = new Set();
|
|
16
|
+
const listeners = new Set();
|
|
17
|
+
let client;
|
|
18
|
+
/** The Neon Auth browser client, created on first use. It talks to the app's `/api/auth`. */
|
|
19
|
+
export function authClient() {
|
|
20
|
+
client ??= createAuthClient();
|
|
21
|
+
return client;
|
|
22
|
+
}
|
|
23
|
+
function update(change) {
|
|
24
|
+
snapshot = { ...snapshot, ...change };
|
|
25
|
+
for (const listener of listeners) {
|
|
26
|
+
listener();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function subscribe(listener) {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
sessionLoad ??= loadSession();
|
|
32
|
+
return () => {
|
|
33
|
+
listeners.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function getSnapshot() {
|
|
37
|
+
return snapshot;
|
|
38
|
+
}
|
|
39
|
+
export function getServerSnapshot() {
|
|
40
|
+
return SERVER_SNAPSHOT;
|
|
41
|
+
}
|
|
42
|
+
/** Reads the session again, after signing in or out. */
|
|
43
|
+
export function refreshSession() {
|
|
44
|
+
sessionLoad = loadSession();
|
|
45
|
+
return sessionLoad;
|
|
46
|
+
}
|
|
47
|
+
/** Forgets the session and every `/me` answer, after signing out. */
|
|
48
|
+
export function clearSession() {
|
|
49
|
+
loadingUsers.clear();
|
|
50
|
+
update({ session: { status: "signed-out" }, users: new Map() });
|
|
51
|
+
}
|
|
52
|
+
async function loadSession() {
|
|
53
|
+
let session;
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(`${AUTH_API_PATH}/get-session`, {
|
|
56
|
+
credentials: "same-origin",
|
|
57
|
+
cache: "no-store",
|
|
58
|
+
headers: { accept: "application/json" },
|
|
59
|
+
});
|
|
60
|
+
session = response.ok
|
|
61
|
+
? sessionFrom(await readJson(response))
|
|
62
|
+
: response.status === 401
|
|
63
|
+
? { status: "signed-out" }
|
|
64
|
+
: { status: "unavailable" };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
session = { status: "unavailable" };
|
|
68
|
+
}
|
|
69
|
+
update({ session });
|
|
70
|
+
}
|
|
71
|
+
function sessionFrom(payload) {
|
|
72
|
+
if (typeof payload !== "object" || payload === null) {
|
|
73
|
+
return { status: "signed-out" };
|
|
74
|
+
}
|
|
75
|
+
const { session, user } = payload;
|
|
76
|
+
if (typeof session !== "object" ||
|
|
77
|
+
session === null ||
|
|
78
|
+
typeof user !== "object" ||
|
|
79
|
+
user === null) {
|
|
80
|
+
return { status: "signed-out" };
|
|
81
|
+
}
|
|
82
|
+
const { id: sessionId } = session;
|
|
83
|
+
const { id, email, name } = user;
|
|
84
|
+
if (typeof sessionId !== "string" || typeof id !== "string") {
|
|
85
|
+
return { status: "signed-out" };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
status: "signed-in",
|
|
89
|
+
sessionId,
|
|
90
|
+
user: {
|
|
91
|
+
id,
|
|
92
|
+
email: typeof email === "string" ? email : null,
|
|
93
|
+
name: typeof name === "string" ? name : null,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Starts loading `/me` for a session unless it is loaded or loading. */
|
|
98
|
+
export function ensureUser(sessionId) {
|
|
99
|
+
if (snapshot.users.has(sessionId) || loadingUsers.has(sessionId)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
loadingUsers.add(sessionId);
|
|
103
|
+
void loadUser().then((state) => {
|
|
104
|
+
if (!loadingUsers.delete(sessionId)) {
|
|
105
|
+
return; // Signed out meanwhile.
|
|
106
|
+
}
|
|
107
|
+
update({ users: new Map(snapshot.users).set(sessionId, state) });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** The session's JWT from Neon Auth, then the ontology's account of the user. */
|
|
111
|
+
async function loadUser() {
|
|
112
|
+
try {
|
|
113
|
+
const tokenResponse = await fetch(`${AUTH_API_PATH}/token`, {
|
|
114
|
+
credentials: "same-origin",
|
|
115
|
+
cache: "no-store",
|
|
116
|
+
headers: { accept: "application/json" },
|
|
117
|
+
});
|
|
118
|
+
if (tokenResponse.status === 401) {
|
|
119
|
+
return SIGNED_OUT_USER;
|
|
120
|
+
}
|
|
121
|
+
const tokenPayload = await readJson(tokenResponse);
|
|
122
|
+
const token = typeof tokenPayload === "object" && tokenPayload !== null
|
|
123
|
+
? tokenPayload.token
|
|
124
|
+
: undefined;
|
|
125
|
+
if (!tokenResponse.ok || typeof token !== "string" || token === "") {
|
|
126
|
+
return failed("auth_unavailable", 503, "Neon Auth did not issue a token. Try again.");
|
|
127
|
+
}
|
|
128
|
+
const response = await fetch(ME_PATH, {
|
|
129
|
+
cache: "no-store",
|
|
130
|
+
headers: { accept: "application/json", authorization: `Bearer ${token}` },
|
|
131
|
+
});
|
|
132
|
+
const payload = await readJson(response);
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
return {
|
|
135
|
+
status: "error",
|
|
136
|
+
user: null,
|
|
137
|
+
error: apiErrorFrom(response.status, payload, "The ontology service rejected the user."),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const user = parseMe(payload);
|
|
141
|
+
return user === null
|
|
142
|
+
? failed("ontology_invalid_response", 502, "GET /api/ontology/me answered an unexpected body.")
|
|
143
|
+
: { status: "signed-in", user, error: null };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return failed("ontology_unreachable", 503, "The app's ontology could not be reached.", error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function failed(code, status, detail, cause) {
|
|
150
|
+
return {
|
|
151
|
+
status: "error",
|
|
152
|
+
user: null,
|
|
153
|
+
error: new SegmentStreamApiError({ code, status, detail, retryable: true, cause }),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @segmentstream-cloud/app/ui: neutral styles for the sign-in form and the user menu.
|
|
3
|
+
* Each rule is one class, so the app's own class selectors override it; the custom properties
|
|
4
|
+
* below change colours, radius and font in one place.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--ss-font: inherit;
|
|
9
|
+
--ss-text: #111111;
|
|
10
|
+
--ss-muted: #5f5f5f;
|
|
11
|
+
--ss-surface: #ffffff;
|
|
12
|
+
--ss-border: #d4d4d4;
|
|
13
|
+
--ss-accent: #111111;
|
|
14
|
+
--ss-accent-text: #ffffff;
|
|
15
|
+
--ss-danger: #b42318;
|
|
16
|
+
--ss-focus: #2563eb;
|
|
17
|
+
--ss-radius: 0.5rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
--ss-text: #f5f5f5;
|
|
23
|
+
--ss-muted: #a3a3a3;
|
|
24
|
+
--ss-surface: #171717;
|
|
25
|
+
--ss-border: #3f3f3f;
|
|
26
|
+
--ss-accent: #f5f5f5;
|
|
27
|
+
--ss-accent-text: #111111;
|
|
28
|
+
--ss-danger: #f97066;
|
|
29
|
+
--ss-focus: #60a5fa;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ss-card {
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
width: min(24rem, 100%);
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
padding: 2rem;
|
|
38
|
+
border: 1px solid var(--ss-border);
|
|
39
|
+
border-radius: calc(var(--ss-radius) * 2);
|
|
40
|
+
background: var(--ss-surface);
|
|
41
|
+
color: var(--ss-text);
|
|
42
|
+
font-family: var(--ss-font);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.ss-title {
|
|
46
|
+
margin: 0 0 0.5rem;
|
|
47
|
+
font-size: 1.5rem;
|
|
48
|
+
line-height: 1.25;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.ss-text {
|
|
52
|
+
margin: 0 0 1.25rem;
|
|
53
|
+
color: var(--ss-muted);
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ss-text strong {
|
|
58
|
+
color: var(--ss-text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ss-form {
|
|
62
|
+
display: grid;
|
|
63
|
+
gap: 1rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ss-field {
|
|
67
|
+
display: grid;
|
|
68
|
+
gap: 0.375rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.ss-label {
|
|
72
|
+
font-size: 0.875rem;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ss-input {
|
|
77
|
+
box-sizing: border-box;
|
|
78
|
+
width: 100%;
|
|
79
|
+
padding: 0.625rem 0.75rem;
|
|
80
|
+
border: 1px solid var(--ss-border);
|
|
81
|
+
border-radius: var(--ss-radius);
|
|
82
|
+
background: transparent;
|
|
83
|
+
color: inherit;
|
|
84
|
+
font: inherit;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ss-input--code {
|
|
88
|
+
font-size: 1.5rem;
|
|
89
|
+
font-variant-numeric: tabular-nums;
|
|
90
|
+
letter-spacing: 0.5em;
|
|
91
|
+
text-align: center;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.ss-input[aria-invalid="true"] {
|
|
95
|
+
border-color: var(--ss-danger);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.ss-input:focus-visible,
|
|
99
|
+
.ss-button:focus-visible,
|
|
100
|
+
.ss-link:focus-visible,
|
|
101
|
+
.ss-user-menu-trigger:focus-visible {
|
|
102
|
+
outline: 2px solid var(--ss-focus);
|
|
103
|
+
outline-offset: 2px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.ss-button {
|
|
107
|
+
display: inline-flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
box-sizing: border-box;
|
|
111
|
+
padding: 0.625rem 1rem;
|
|
112
|
+
border: 1px solid var(--ss-accent);
|
|
113
|
+
border-radius: var(--ss-radius);
|
|
114
|
+
background: var(--ss-accent);
|
|
115
|
+
color: var(--ss-accent-text);
|
|
116
|
+
font: inherit;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
text-decoration: none;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ss-button--secondary {
|
|
123
|
+
background: transparent;
|
|
124
|
+
color: var(--ss-text);
|
|
125
|
+
border-color: var(--ss-border);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ss-button:disabled,
|
|
129
|
+
.ss-link:disabled {
|
|
130
|
+
cursor: not-allowed;
|
|
131
|
+
opacity: 0.6;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ss-link {
|
|
135
|
+
padding: 0;
|
|
136
|
+
border: 0;
|
|
137
|
+
background: none;
|
|
138
|
+
color: var(--ss-text);
|
|
139
|
+
font: inherit;
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
text-decoration: underline;
|
|
142
|
+
text-underline-offset: 0.2em;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.ss-actions {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
gap: 1rem;
|
|
150
|
+
align-items: center;
|
|
151
|
+
margin-top: 1.25rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ss-error {
|
|
155
|
+
margin: 0;
|
|
156
|
+
color: var(--ss-danger);
|
|
157
|
+
font-size: 0.875rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ss-notice {
|
|
161
|
+
margin: 0;
|
|
162
|
+
color: var(--ss-muted);
|
|
163
|
+
font-size: 0.875rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.ss-user-menu {
|
|
167
|
+
position: relative;
|
|
168
|
+
display: inline-block;
|
|
169
|
+
color: var(--ss-text);
|
|
170
|
+
font-family: var(--ss-font);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.ss-user-menu-trigger {
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
gap: 0.5rem;
|
|
176
|
+
align-items: center;
|
|
177
|
+
padding: 0.25rem 0.5rem 0.25rem 0.25rem;
|
|
178
|
+
border: 1px solid var(--ss-border);
|
|
179
|
+
border-radius: 999px;
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
list-style: none;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.ss-user-menu-trigger::-webkit-details-marker {
|
|
185
|
+
display: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.ss-avatar {
|
|
189
|
+
display: inline-grid;
|
|
190
|
+
place-items: center;
|
|
191
|
+
width: 2rem;
|
|
192
|
+
height: 2rem;
|
|
193
|
+
border-radius: 50%;
|
|
194
|
+
background: var(--ss-accent);
|
|
195
|
+
color: var(--ss-accent-text);
|
|
196
|
+
font-size: 0.75rem;
|
|
197
|
+
font-weight: 700;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.ss-user-menu-label {
|
|
201
|
+
max-width: 12rem;
|
|
202
|
+
overflow: hidden;
|
|
203
|
+
font-size: 0.875rem;
|
|
204
|
+
text-overflow: ellipsis;
|
|
205
|
+
white-space: nowrap;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.ss-user-menu-panel {
|
|
209
|
+
position: absolute;
|
|
210
|
+
right: 0;
|
|
211
|
+
z-index: 10;
|
|
212
|
+
display: grid;
|
|
213
|
+
gap: 0.5rem;
|
|
214
|
+
min-width: 14rem;
|
|
215
|
+
margin-top: 0.5rem;
|
|
216
|
+
padding: 1rem;
|
|
217
|
+
border: 1px solid var(--ss-border);
|
|
218
|
+
border-radius: var(--ss-radius);
|
|
219
|
+
background: var(--ss-surface);
|
|
220
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.ss-user-menu-name {
|
|
224
|
+
margin: 0;
|
|
225
|
+
font-weight: 600;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.ss-user-menu-email {
|
|
229
|
+
margin: 0;
|
|
230
|
+
color: var(--ss-muted);
|
|
231
|
+
font-size: 0.875rem;
|
|
232
|
+
overflow-wrap: anywhere;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.ss-badges {
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-wrap: wrap;
|
|
238
|
+
gap: 0.25rem;
|
|
239
|
+
margin: 0;
|
|
240
|
+
padding: 0;
|
|
241
|
+
list-style: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.ss-badge {
|
|
245
|
+
padding: 0.125rem 0.5rem;
|
|
246
|
+
border: 1px solid var(--ss-border);
|
|
247
|
+
border-radius: 999px;
|
|
248
|
+
font-size: 0.75rem;
|
|
249
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sign-in,
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Sign-in, users, permissions, the ontology client and warehouse data access for SegmentStream workspace apps built with Next.js.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|
|
7
7
|
"homepage": "https://segmentstream.com",
|
|
@@ -13,14 +13,21 @@
|
|
|
13
13
|
"keywords": [
|
|
14
14
|
"segmentstream",
|
|
15
15
|
"nextjs",
|
|
16
|
-
"authentication"
|
|
16
|
+
"authentication",
|
|
17
|
+
"neon-auth",
|
|
18
|
+
"ontology"
|
|
17
19
|
],
|
|
18
20
|
"type": "module",
|
|
19
21
|
"exports": {
|
|
20
22
|
".": {
|
|
21
23
|
"types": "./dist/index.d.ts",
|
|
22
24
|
"import": "./dist/index.js"
|
|
23
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"./ui": {
|
|
27
|
+
"types": "./dist/ui/index.d.ts",
|
|
28
|
+
"import": "./dist/ui/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./ui/styles.css": "./dist/ui/styles.css"
|
|
24
31
|
},
|
|
25
32
|
"files": [
|
|
26
33
|
"dist",
|
|
@@ -28,26 +35,36 @@
|
|
|
28
35
|
"LICENSE",
|
|
29
36
|
"NOTICE"
|
|
30
37
|
],
|
|
31
|
-
"sideEffects":
|
|
38
|
+
"sideEffects": [
|
|
39
|
+
"**/*.css"
|
|
40
|
+
],
|
|
32
41
|
"engines": {
|
|
33
42
|
"node": ">=20"
|
|
34
43
|
},
|
|
35
44
|
"scripts": {
|
|
36
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
45
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/ui/styles.css dist/ui/styles.css",
|
|
37
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
47
|
"lint": "eslint src tests --config ../../eslint.config.mjs",
|
|
39
48
|
"test": "vitest run",
|
|
40
49
|
"prepack": "npm run build"
|
|
41
50
|
},
|
|
42
51
|
"dependencies": {
|
|
52
|
+
"@neondatabase/auth": "0.5.0-beta",
|
|
43
53
|
"jose": "^6.2.12"
|
|
44
54
|
},
|
|
45
55
|
"peerDependencies": {
|
|
46
|
-
"next": ">=16.0.0"
|
|
56
|
+
"next": ">=16.0.0",
|
|
57
|
+
"react": ">=19.0.0"
|
|
47
58
|
},
|
|
48
59
|
"devDependencies": {
|
|
60
|
+
"@testing-library/react": "^16.3.2",
|
|
49
61
|
"@types/node": "^24",
|
|
62
|
+
"@types/react": "^19",
|
|
63
|
+
"@types/react-dom": "^19",
|
|
64
|
+
"jsdom": "^28.1.0",
|
|
50
65
|
"next": "16.3.8",
|
|
66
|
+
"react": "19.2.8",
|
|
67
|
+
"react-dom": "19.2.8",
|
|
51
68
|
"typescript": "^5.9",
|
|
52
69
|
"vitest": "^4.1.11"
|
|
53
70
|
},
|