@archiva/archiva-nextjs 0.1.2 → 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.
package/README.md CHANGED
@@ -22,22 +22,25 @@ ARCHIVA_SECRET_KEY=pk_test_xxxxx
22
22
 
23
23
  ## Usage
24
24
 
25
- ### ArchivaProvider (Server Component)
25
+ ### ArchivaProvider (Client Component)
26
26
 
27
- Wrap your application (or specific routes) with the ArchivaProvider component:
27
+ Wrap your application (or specific routes) with the ArchivaProvider component. The provider makes the API key available to child components via React context:
28
28
 
29
29
  ```tsx
30
30
  import { ArchivaProvider } from '@archiva/archiva-nextjs';
31
31
 
32
32
  export default function Layout({ children }) {
33
+ // API key can be passed as prop, or it will use ARCHIVA_SECRET_KEY env var
33
34
  return (
34
- <ArchivaProvider>
35
+ <ArchivaProvider apiKey="pk_test_xxxxx">
35
36
  {children}
36
37
  </ArchivaProvider>
37
38
  );
38
39
  }
39
40
  ```
40
41
 
42
+ **Note:** If you don't pass an `apiKey` prop, the provider will use the `ARCHIVA_SECRET_KEY` environment variable. The Timeline component and server actions will automatically use the API key from the provider context.
43
+
41
44
  ### Server Actions
42
45
 
43
46
  #### loadEvents
@@ -112,7 +115,7 @@ const result = await createEvents([
112
115
 
113
116
  ### Timeline Component (Client Component)
114
117
 
115
- Display a timeline of audit events:
118
+ Display a timeline of audit events. The Timeline component automatically uses the API key from the ArchivaProvider context:
116
119
 
117
120
  ```tsx
118
121
  'use client';
@@ -130,6 +133,8 @@ export function InvoiceTimeline({ invoiceId }: { invoiceId: string }) {
130
133
  }
131
134
  ```
132
135
 
136
+ **Note:** The Timeline component must be used within an `ArchivaProvider`. It no longer requires an `apiKey` prop as it gets the API key from the provider context.
137
+
133
138
  ## API Reference
134
139
 
135
140
  ### LoadEventsParams
@@ -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,19 +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-only provider component for Archiva.
10
- * This component validates the API key format and provides configuration
11
- * for server actions (loadEvents, createEvents).
12
- *
13
- * The API key should be passed via props or set as ARCHIVA_SECRET_KEY environment variable.
14
- */
15
- declare function ArchivaProvider({ children }: ArchivaProviderProps): react_jsx_runtime.JSX.Element;
16
-
17
5
  type EventChange = {
18
6
  op: "set" | "unset" | "add" | "remove" | "replace" | string;
19
7
  path: string;
@@ -70,6 +58,27 @@ declare class ArchivaError extends Error {
70
58
  });
71
59
  }
72
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
+
73
82
  /**
74
83
  * Server action to load audit events
75
84
  *
@@ -118,8 +127,7 @@ type TimelineProps = {
118
127
  initialLimit?: number;
119
128
  className?: string;
120
129
  emptyMessage?: string;
121
- apiKey?: string;
122
130
  };
123
- declare function Timeline({ entityId, actorId, entityType, initialLimit, className, emptyMessage, apiKey, }: TimelineProps): react_jsx_runtime.JSX.Element;
131
+ declare function Timeline({ entityId, actorId, entityType, initialLimit, className, emptyMessage, }: TimelineProps): react_jsx_runtime.JSX.Element;
124
132
 
125
- 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 };
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,19 +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-only provider component for Archiva.
10
- * This component validates the API key format and provides configuration
11
- * for server actions (loadEvents, createEvents).
12
- *
13
- * The API key should be passed via props or set as ARCHIVA_SECRET_KEY environment variable.
14
- */
15
- declare function ArchivaProvider({ children }: ArchivaProviderProps): react_jsx_runtime.JSX.Element;
16
-
17
5
  type EventChange = {
18
6
  op: "set" | "unset" | "add" | "remove" | "replace" | string;
19
7
  path: string;
@@ -70,6 +58,27 @@ declare class ArchivaError extends Error {
70
58
  });
71
59
  }
72
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
+
73
82
  /**
74
83
  * Server action to load audit events
75
84
  *
@@ -118,8 +127,7 @@ type TimelineProps = {
118
127
  initialLimit?: number;
119
128
  className?: string;
120
129
  emptyMessage?: string;
121
- apiKey?: string;
122
130
  };
123
- declare function Timeline({ entityId, actorId, entityType, initialLimit, className, emptyMessage, apiKey, }: TimelineProps): react_jsx_runtime.JSX.Element;
131
+ declare function Timeline({ entityId, actorId, entityType, initialLimit, className, emptyMessage, }: TimelineProps): react_jsx_runtime.JSX.Element;
124
132
 
125
- 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 };
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 };