@archiva/archiva-nextjs 0.1.3 → 0.1.4

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.
@@ -0,0 +1,398 @@
1
+ // src/react/ArchivaProvider.tsx
2
+ import "server-only";
3
+
4
+ // src/react/internal/ArchivaProviderClient.tsx
5
+ import * as React2 from "react";
6
+
7
+ // src/react/context.tsx
8
+ import * as React from "react";
9
+ var ArchivaContext = React.createContext(void 0);
10
+ function useArchivaContext() {
11
+ const context = React.useContext(ArchivaContext);
12
+ if (context === void 0) {
13
+ throw new Error("useArchivaContext must be used within an ArchivaProvider");
14
+ }
15
+ return context;
16
+ }
17
+
18
+ // src/react/internal/ArchivaProviderClient.tsx
19
+ import { jsx } from "react/jsx-runtime";
20
+ function ArchivaProviderClient({
21
+ children,
22
+ apiBaseUrl,
23
+ tokenEndpoint,
24
+ projectId
25
+ }) {
26
+ const [tokenCache, setTokenCache] = React2.useState(null);
27
+ const [isRefreshing, setIsRefreshing] = React2.useState(false);
28
+ const refreshTimeoutRef = React2.useRef(null);
29
+ React2.useEffect(() => {
30
+ return () => {
31
+ if (refreshTimeoutRef.current) {
32
+ clearTimeout(refreshTimeoutRef.current);
33
+ }
34
+ };
35
+ }, []);
36
+ const fetchToken = React2.useCallback(async () => {
37
+ const baseUrl = tokenEndpoint.startsWith("http") ? tokenEndpoint : typeof window !== "undefined" ? `${window.location.origin}${tokenEndpoint}` : `${apiBaseUrl}${tokenEndpoint}`;
38
+ const url = new URL(baseUrl);
39
+ if (projectId) {
40
+ url.searchParams.set("projectId", projectId);
41
+ }
42
+ const response = await fetch(url.toString(), {
43
+ method: "GET",
44
+ credentials: "include"
45
+ });
46
+ if (!response.ok) {
47
+ const error = await response.json().catch(() => ({ error: "Failed to fetch token" }));
48
+ throw new Error(error.error || "Failed to fetch frontend token");
49
+ }
50
+ const data = await response.json();
51
+ if (!data.token || !data.expiresAt) {
52
+ throw new Error("Invalid token response");
53
+ }
54
+ return data.token;
55
+ }, [tokenEndpoint, projectId, apiBaseUrl]);
56
+ const getToken = React2.useCallback(async () => {
57
+ const now = Math.floor(Date.now() / 1e3);
58
+ if (tokenCache && tokenCache.expiresAt > now + 30) {
59
+ return tokenCache.token;
60
+ }
61
+ if (isRefreshing) {
62
+ await new Promise((resolve) => setTimeout(resolve, 100));
63
+ return getToken();
64
+ }
65
+ setIsRefreshing(true);
66
+ try {
67
+ const token = await fetchToken();
68
+ const expiresAt = getTokenExpiry(token);
69
+ if (!expiresAt) {
70
+ throw new Error("Failed to parse token expiry");
71
+ }
72
+ const newCache = { token, expiresAt };
73
+ setTokenCache(newCache);
74
+ const refreshIn = Math.max(0, expiresAt - now - 30) * 1e3;
75
+ if (refreshTimeoutRef.current) {
76
+ clearTimeout(refreshTimeoutRef.current);
77
+ }
78
+ refreshTimeoutRef.current = setTimeout(() => {
79
+ setTokenCache(null);
80
+ }, refreshIn);
81
+ return token;
82
+ } finally {
83
+ setIsRefreshing(false);
84
+ }
85
+ }, [tokenCache, isRefreshing, fetchToken]);
86
+ const forceRefreshToken = React2.useCallback(async () => {
87
+ setTokenCache(null);
88
+ if (refreshTimeoutRef.current) {
89
+ clearTimeout(refreshTimeoutRef.current);
90
+ refreshTimeoutRef.current = null;
91
+ }
92
+ setIsRefreshing(true);
93
+ try {
94
+ const token = await fetchToken();
95
+ const expiresAt = getTokenExpiry(token);
96
+ if (!expiresAt) {
97
+ throw new Error("Failed to parse token expiry");
98
+ }
99
+ const newCache = { token, expiresAt };
100
+ setTokenCache(newCache);
101
+ const now = Math.floor(Date.now() / 1e3);
102
+ const refreshIn = Math.max(0, expiresAt - now - 30) * 1e3;
103
+ if (refreshTimeoutRef.current) {
104
+ clearTimeout(refreshTimeoutRef.current);
105
+ }
106
+ refreshTimeoutRef.current = setTimeout(() => {
107
+ setTokenCache(null);
108
+ }, refreshIn);
109
+ return token;
110
+ } finally {
111
+ setIsRefreshing(false);
112
+ }
113
+ }, [fetchToken]);
114
+ const value = React2.useMemo(
115
+ () => ({
116
+ apiBaseUrl,
117
+ getToken,
118
+ forceRefreshToken,
119
+ projectId
120
+ }),
121
+ [apiBaseUrl, getToken, forceRefreshToken, projectId]
122
+ );
123
+ return /* @__PURE__ */ jsx(ArchivaContext.Provider, { value, children });
124
+ }
125
+ function getTokenExpiry(token) {
126
+ try {
127
+ const parts = token.split(".");
128
+ if (parts.length !== 3) {
129
+ return null;
130
+ }
131
+ const payload = JSON.parse(
132
+ atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"))
133
+ );
134
+ return payload.exp;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ // src/react/ArchivaProvider.tsx
141
+ import { jsx as jsx2 } from "react/jsx-runtime";
142
+ function ArchivaProvider({
143
+ children,
144
+ apiBaseUrl = "https://api.archiva.app",
145
+ tokenEndpoint = "/api/archiva/frontend-token",
146
+ projectId
147
+ }) {
148
+ if (!process.env.ARCHIVA_SECRET_KEY) {
149
+ throw new Error(
150
+ "ARCHIVA_SECRET_KEY environment variable is required. Set it in your .env.local file."
151
+ );
152
+ }
153
+ return /* @__PURE__ */ jsx2(
154
+ ArchivaProviderClient,
155
+ {
156
+ apiBaseUrl,
157
+ tokenEndpoint,
158
+ projectId,
159
+ children
160
+ }
161
+ );
162
+ }
163
+
164
+ // src/react/Timeline.tsx
165
+ import * as React3 from "react";
166
+
167
+ // src/react/useArchiva.ts
168
+ function useArchiva() {
169
+ return useArchivaContext();
170
+ }
171
+
172
+ // src/react/Timeline.tsx
173
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
174
+ function formatTimestamp(timestamp) {
175
+ const date = typeof timestamp === "string" ? new Date(timestamp) : timestamp;
176
+ return date.toLocaleDateString(void 0, {
177
+ month: "short",
178
+ day: "numeric",
179
+ year: "numeric",
180
+ hour: "numeric",
181
+ minute: "2-digit"
182
+ });
183
+ }
184
+ function eventToTimelineItem(event) {
185
+ return {
186
+ id: event.id,
187
+ title: event.action,
188
+ description: `${event.entityType} ${event.entityId}`,
189
+ timestamp: event.receivedAt,
190
+ badge: event.actorId ?? void 0,
191
+ data: event
192
+ };
193
+ }
194
+ async function fetchEventsWithRetry(apiBaseUrl, getToken, forceRefreshToken, params) {
195
+ const url = new URL(`${apiBaseUrl}/api/events`);
196
+ if (params.entityId) {
197
+ url.searchParams.set("entityId", params.entityId);
198
+ }
199
+ if (params.actorId) {
200
+ url.searchParams.set("actorId", params.actorId);
201
+ }
202
+ if (params.entityType) {
203
+ url.searchParams.set("entityType", params.entityType);
204
+ }
205
+ if (params.limit) {
206
+ url.searchParams.set("limit", String(params.limit));
207
+ }
208
+ if (params.cursor) {
209
+ url.searchParams.set("cursor", params.cursor);
210
+ }
211
+ const makeRequest = async (token2) => {
212
+ return fetch(url.toString(), {
213
+ headers: {
214
+ Authorization: `Bearer ${token2}`
215
+ }
216
+ });
217
+ };
218
+ const token = await getToken();
219
+ let response = await makeRequest(token);
220
+ if (response.status === 401 || response.status === 403) {
221
+ const newToken = await forceRefreshToken();
222
+ response = await makeRequest(newToken);
223
+ }
224
+ if (!response.ok) {
225
+ const error = await response.json().catch(() => ({ error: response.statusText }));
226
+ throw new Error(error.error || `HTTP ${response.status}`);
227
+ }
228
+ const payload = await response.json();
229
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.items)) {
230
+ throw new Error("Invalid response format");
231
+ }
232
+ return {
233
+ items: payload.items,
234
+ nextCursor: typeof payload.nextCursor === "string" ? payload.nextCursor : void 0
235
+ };
236
+ }
237
+ function Timeline({
238
+ entityId,
239
+ actorId,
240
+ entityType,
241
+ initialLimit = 25,
242
+ className,
243
+ emptyMessage = "No events yet."
244
+ }) {
245
+ const { apiBaseUrl, getToken, forceRefreshToken } = useArchiva();
246
+ const [items, setItems] = React3.useState([]);
247
+ const [cursor, setCursor] = React3.useState(void 0);
248
+ const [loading, setLoading] = React3.useState(false);
249
+ const [error, setError] = React3.useState(null);
250
+ const [hasMore, setHasMore] = React3.useState(false);
251
+ const load = React3.useCallback(
252
+ async (options) => {
253
+ setLoading(true);
254
+ setError(null);
255
+ try {
256
+ const params = {
257
+ entityId,
258
+ actorId,
259
+ entityType,
260
+ limit: initialLimit,
261
+ cursor: options?.reset ? void 0 : options?.currentCursor ?? cursor
262
+ };
263
+ const response = await fetchEventsWithRetry(
264
+ apiBaseUrl,
265
+ getToken,
266
+ forceRefreshToken,
267
+ params
268
+ );
269
+ const newItems = response.items.map(eventToTimelineItem);
270
+ setItems((prev) => options?.reset ? newItems : [...prev, ...newItems]);
271
+ setCursor(response.nextCursor);
272
+ setHasMore(Boolean(response.nextCursor));
273
+ } catch (err) {
274
+ setError(err.message);
275
+ } finally {
276
+ setLoading(false);
277
+ }
278
+ },
279
+ [entityId, actorId, entityType, initialLimit, apiBaseUrl, getToken, forceRefreshToken, cursor]
280
+ );
281
+ React3.useEffect(() => {
282
+ setCursor(void 0);
283
+ setItems([]);
284
+ void load({ reset: true });
285
+ }, [entityId, actorId, entityType]);
286
+ if (loading && items.length === 0) {
287
+ return /* @__PURE__ */ jsx3("div", { className: className || "", children: /* @__PURE__ */ jsx3("div", { children: "Loading events..." }) });
288
+ }
289
+ if (error) {
290
+ return /* @__PURE__ */ jsx3("div", { className: className || "", children: /* @__PURE__ */ jsxs("div", { style: { color: "red" }, children: [
291
+ "Error: ",
292
+ error
293
+ ] }) });
294
+ }
295
+ if (items.length === 0) {
296
+ return /* @__PURE__ */ jsx3("div", { className: className || "", children: /* @__PURE__ */ jsx3("div", { children: emptyMessage }) });
297
+ }
298
+ return /* @__PURE__ */ jsxs("div", { className: className || "", style: { width: "100%" }, children: [
299
+ /* @__PURE__ */ jsx3("div", { style: { position: "relative" }, children: items.map((item, index) => /* @__PURE__ */ jsxs(
300
+ "div",
301
+ {
302
+ style: {
303
+ position: "relative",
304
+ display: "flex",
305
+ gap: "1rem",
306
+ paddingBottom: index < items.length - 1 ? "2rem" : "0"
307
+ },
308
+ children: [
309
+ index < items.length - 1 && /* @__PURE__ */ jsx3(
310
+ "div",
311
+ {
312
+ style: {
313
+ position: "absolute",
314
+ left: "1.25rem",
315
+ top: "3rem",
316
+ height: "calc(100% - 3rem)",
317
+ width: "2px",
318
+ backgroundColor: "#e5e7eb"
319
+ }
320
+ }
321
+ ),
322
+ /* @__PURE__ */ jsx3(
323
+ "div",
324
+ {
325
+ style: {
326
+ position: "relative",
327
+ zIndex: 10,
328
+ width: "2.5rem",
329
+ height: "2.5rem",
330
+ borderRadius: "50%",
331
+ backgroundColor: "#f3f4f6",
332
+ border: "2px solid #fff",
333
+ display: "flex",
334
+ alignItems: "center",
335
+ justifyContent: "center",
336
+ flexShrink: 0
337
+ },
338
+ children: /* @__PURE__ */ jsx3(
339
+ "div",
340
+ {
341
+ style: {
342
+ width: "0.75rem",
343
+ height: "0.75rem",
344
+ borderRadius: "50%",
345
+ backgroundColor: "#6b7280"
346
+ }
347
+ }
348
+ )
349
+ }
350
+ ),
351
+ /* @__PURE__ */ jsx3("div", { style: { flex: 1, paddingBottom: "2rem" }, children: /* @__PURE__ */ jsx3("div", { style: { display: "flex", alignItems: "start", justifyContent: "space-between", gap: "0.5rem" }, children: /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
352
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
353
+ /* @__PURE__ */ jsx3("h4", { style: { fontWeight: 600, margin: 0 }, children: item.title }),
354
+ item.badge && /* @__PURE__ */ jsx3(
355
+ "span",
356
+ {
357
+ style: {
358
+ fontSize: "0.75rem",
359
+ padding: "0.125rem 0.5rem",
360
+ borderRadius: "0.375rem",
361
+ backgroundColor: "#f3f4f6"
362
+ },
363
+ children: item.badge
364
+ }
365
+ )
366
+ ] }),
367
+ /* @__PURE__ */ jsx3("p", { style: { fontSize: "0.875rem", color: "#6b7280", margin: "0.25rem 0 0 0" }, children: formatTimestamp(item.timestamp) }),
368
+ item.description && /* @__PURE__ */ jsx3("p", { style: { fontSize: "0.875rem", color: "#6b7280", margin: "0.5rem 0 0 0" }, children: item.description })
369
+ ] }) }) })
370
+ ]
371
+ },
372
+ item.id
373
+ )) }),
374
+ hasMore && /* @__PURE__ */ jsx3("div", { style: { marginTop: "1rem" }, children: /* @__PURE__ */ jsx3(
375
+ "button",
376
+ {
377
+ type: "button",
378
+ onClick: () => load(),
379
+ disabled: loading,
380
+ style: {
381
+ padding: "0.5rem 1rem",
382
+ backgroundColor: loading ? "#d1d5db" : "#3b82f6",
383
+ color: "white",
384
+ border: "none",
385
+ borderRadius: "0.375rem",
386
+ cursor: loading ? "not-allowed" : "pointer"
387
+ },
388
+ children: loading ? "Loading..." : "Load more"
389
+ }
390
+ ) })
391
+ ] });
392
+ }
393
+
394
+ export {
395
+ ArchivaProvider,
396
+ useArchiva,
397
+ Timeline
398
+ };
package/dist/index.d.mts CHANGED
@@ -1,27 +1,7 @@
1
+ export { ArchivaProvider, ArchivaProviderProps, Timeline, TimelineItem, TimelineProps, useArchiva } from './react/index.mjs';
1
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
3
  import { ReactNode } from 'react';
3
4
 
4
- type ArchivaProviderProps = {
5
- children: ReactNode;
6
- apiKey?: string;
7
- };
8
- /**
9
- * Server component wrapper that reads ARCHIVA_SECRET_KEY from environment
10
- * and passes it to the client provider.
11
- *
12
- * The API key can be:
13
- * - Passed via props (takes precedence)
14
- * - Set as ARCHIVA_SECRET_KEY environment variable (automatically read)
15
- *
16
- * Child components like Timeline will automatically use the API key from this context.
17
- */
18
- declare function ArchivaProvider({ children, apiKey }: ArchivaProviderProps): react_jsx_runtime.JSX.Element;
19
-
20
- type ArchivaContextValue = {
21
- apiKey: string | undefined;
22
- };
23
- declare function useArchivaContext(): ArchivaContextValue;
24
-
25
5
  type EventChange = {
26
6
  op: "set" | "unset" | "add" | "remove" | "replace" | string;
27
7
  path: string;
@@ -78,6 +58,27 @@ declare class ArchivaError extends Error {
78
58
  });
79
59
  }
80
60
 
61
+ type ArchivaProviderProps = {
62
+ children: ReactNode;
63
+ apiKey?: string;
64
+ };
65
+ /**
66
+ * Server component wrapper that reads ARCHIVA_SECRET_KEY from environment
67
+ * and passes it to the client provider.
68
+ *
69
+ * The API key can be:
70
+ * - Passed via props (takes precedence)
71
+ * - Set as ARCHIVA_SECRET_KEY environment variable (automatically read)
72
+ *
73
+ * Child components like Timeline will automatically use the API key from this context.
74
+ */
75
+ declare function ArchivaProvider({ children, apiKey }: ArchivaProviderProps): react_jsx_runtime.JSX.Element;
76
+
77
+ type ArchivaContextValue = {
78
+ apiKey: string | undefined;
79
+ };
80
+ declare function useArchivaContext(): ArchivaContextValue;
81
+
81
82
  /**
82
83
  * Server action to load audit events
83
84
  *
@@ -129,4 +130,4 @@ type TimelineProps = {
129
130
  };
130
131
  declare function Timeline({ entityId, actorId, entityType, initialLimit, className, emptyMessage, }: TimelineProps): react_jsx_runtime.JSX.Element;
131
132
 
132
- export { ArchivaError, ArchivaProvider, type ArchivaProviderProps, type AuditEventListItem, type CreateEventInput, type CreateEventOptions, type EventChange, type LoadEventsParams, type PageResult, Timeline, type TimelineItem, type TimelineProps, createEvent, createEvents, loadEvents, useArchivaContext };
133
+ export { ArchivaError, ArchivaProvider as ArchivaProviderLegacy, type ArchivaProviderProps as ArchivaProviderPropsLegacy, type AuditEventListItem, type CreateEventInput, type CreateEventOptions, type EventChange, type LoadEventsParams, type PageResult, type TimelineItem as TimelineItemLegacy, Timeline as TimelineLegacy, type TimelineProps as TimelinePropsLegacy, createEvent, createEvents, loadEvents, useArchivaContext };
package/dist/index.d.ts CHANGED
@@ -1,27 +1,7 @@
1
+ export { ArchivaProvider, ArchivaProviderProps, Timeline, TimelineItem, TimelineProps, useArchiva } from './react/index.js';
1
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
3
  import { ReactNode } from 'react';
3
4
 
4
- type ArchivaProviderProps = {
5
- children: ReactNode;
6
- apiKey?: string;
7
- };
8
- /**
9
- * Server component wrapper that reads ARCHIVA_SECRET_KEY from environment
10
- * and passes it to the client provider.
11
- *
12
- * The API key can be:
13
- * - Passed via props (takes precedence)
14
- * - Set as ARCHIVA_SECRET_KEY environment variable (automatically read)
15
- *
16
- * Child components like Timeline will automatically use the API key from this context.
17
- */
18
- declare function ArchivaProvider({ children, apiKey }: ArchivaProviderProps): react_jsx_runtime.JSX.Element;
19
-
20
- type ArchivaContextValue = {
21
- apiKey: string | undefined;
22
- };
23
- declare function useArchivaContext(): ArchivaContextValue;
24
-
25
5
  type EventChange = {
26
6
  op: "set" | "unset" | "add" | "remove" | "replace" | string;
27
7
  path: string;
@@ -78,6 +58,27 @@ declare class ArchivaError extends Error {
78
58
  });
79
59
  }
80
60
 
61
+ type ArchivaProviderProps = {
62
+ children: ReactNode;
63
+ apiKey?: string;
64
+ };
65
+ /**
66
+ * Server component wrapper that reads ARCHIVA_SECRET_KEY from environment
67
+ * and passes it to the client provider.
68
+ *
69
+ * The API key can be:
70
+ * - Passed via props (takes precedence)
71
+ * - Set as ARCHIVA_SECRET_KEY environment variable (automatically read)
72
+ *
73
+ * Child components like Timeline will automatically use the API key from this context.
74
+ */
75
+ declare function ArchivaProvider({ children, apiKey }: ArchivaProviderProps): react_jsx_runtime.JSX.Element;
76
+
77
+ type ArchivaContextValue = {
78
+ apiKey: string | undefined;
79
+ };
80
+ declare function useArchivaContext(): ArchivaContextValue;
81
+
81
82
  /**
82
83
  * Server action to load audit events
83
84
  *
@@ -129,4 +130,4 @@ type TimelineProps = {
129
130
  };
130
131
  declare function Timeline({ entityId, actorId, entityType, initialLimit, className, emptyMessage, }: TimelineProps): react_jsx_runtime.JSX.Element;
131
132
 
132
- export { ArchivaError, ArchivaProvider, type ArchivaProviderProps, type AuditEventListItem, type CreateEventInput, type CreateEventOptions, type EventChange, type LoadEventsParams, type PageResult, Timeline, type TimelineItem, type TimelineProps, createEvent, createEvents, loadEvents, useArchivaContext };
133
+ export { ArchivaError, ArchivaProvider as ArchivaProviderLegacy, type ArchivaProviderProps as ArchivaProviderPropsLegacy, type AuditEventListItem, type CreateEventInput, type CreateEventOptions, type EventChange, type LoadEventsParams, type PageResult, type TimelineItem as TimelineItemLegacy, Timeline as TimelineLegacy, type TimelineProps as TimelinePropsLegacy, createEvent, createEvents, loadEvents, useArchivaContext };