@archiva/archiva-nextjs 0.1.4 → 0.1.5
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 +295 -96
- package/dist/chunk-OSBJW2ZH.mjs +157 -0
- package/dist/chunk-XZUDW4PU.mjs +15 -0
- package/dist/index.d.mts +4 -76
- package/dist/index.d.ts +4 -76
- package/dist/index.js +2 -615
- package/dist/index.mjs +4 -373
- package/dist/react/client.d.mts +37 -0
- package/dist/react/client.d.ts +37 -0
- package/dist/react/client.js +283 -0
- package/dist/react/client.mjs +238 -0
- package/dist/react/index.d.mts +1 -35
- package/dist/react/index.d.ts +1 -35
- package/dist/react/index.js +2 -243
- package/dist/react/index.mjs +4 -7
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @archiva/archiva-nextjs
|
|
2
2
|
|
|
3
|
-
Next.js SDK for Archiva -
|
|
3
|
+
Next.js SDK for Archiva - Frontend Token Provider and Timeline Component
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -12,7 +12,9 @@ pnpm add @archiva/archiva-nextjs
|
|
|
12
12
|
yarn add @archiva/archiva-nextjs
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### 1. Set Environment Variable
|
|
16
18
|
|
|
17
19
|
Set the `ARCHIVA_SECRET_KEY` environment variable in your `.env.local` file:
|
|
18
20
|
|
|
@@ -20,152 +22,349 @@ Set the `ARCHIVA_SECRET_KEY` environment variable in your `.env.local` file:
|
|
|
20
22
|
ARCHIVA_SECRET_KEY=pk_test_xxxxx
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
**Important:** This should be a valid Archiva API key. The SDK uses this to mint short-lived frontend tokens securely on the server.
|
|
26
|
+
|
|
27
|
+
### 2. Create Token Endpoint Route
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
Create a Next.js API route to handle frontend token requests. This route will be called by the SDK to fetch short-lived tokens.
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
**File:** `app/api/archiva/frontend-token/route.ts`
|
|
28
32
|
|
|
29
33
|
```tsx
|
|
30
|
-
import {
|
|
34
|
+
import { GET } from '@archiva/archiva-nextjs/server';
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<ArchivaProvider apiKey="pk_test_xxxxx">
|
|
36
|
-
{children}
|
|
37
|
-
</ArchivaProvider>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
36
|
+
// Export the GET handler - that's it!
|
|
37
|
+
export { GET };
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
Or with custom configuration:
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
```tsx
|
|
43
|
+
import { createFrontendTokenRoute } from '@archiva/archiva-nextjs/server';
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
export const GET = createFrontendTokenRoute({
|
|
46
|
+
apiBaseUrl: process.env.ARCHIVA_API_URL, // Optional: defaults to https://api.archiva.app
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Wrap Your App with ArchivaProvider
|
|
51
|
+
|
|
52
|
+
Wrap your application layout with the `ArchivaProvider` component. This is a **server component** that validates your `ARCHIVA_SECRET_KEY` and provides the token management context to client components.
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
**File:** `app/layout.tsx`
|
|
49
55
|
|
|
50
56
|
```tsx
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
import { ArchivaProvider } from '@archiva/archiva-nextjs/react';
|
|
58
|
+
|
|
59
|
+
export default function RootLayout({
|
|
60
|
+
children,
|
|
61
|
+
}: {
|
|
62
|
+
children: React.ReactNode;
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<html lang="en">
|
|
66
|
+
<body>
|
|
67
|
+
<ArchivaProvider>
|
|
68
|
+
{children}
|
|
69
|
+
</ArchivaProvider>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
61
74
|
```
|
|
62
75
|
|
|
63
|
-
|
|
76
|
+
**Important:** Always import `ArchivaProvider` from `@archiva/archiva-nextjs/react` (not from the root package). This ensures the import is server-safe and won't trigger RSC errors.
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
With custom configuration:
|
|
66
79
|
|
|
67
80
|
```tsx
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
],
|
|
90
|
-
});
|
|
81
|
+
import { ArchivaProvider } from '@archiva/archiva-nextjs/react';
|
|
82
|
+
|
|
83
|
+
export default function RootLayout({
|
|
84
|
+
children,
|
|
85
|
+
}: {
|
|
86
|
+
children: React.ReactNode;
|
|
87
|
+
}) {
|
|
88
|
+
return (
|
|
89
|
+
<html lang="en">
|
|
90
|
+
<body>
|
|
91
|
+
<ArchivaProvider
|
|
92
|
+
apiBaseUrl="https://api.archiva.app"
|
|
93
|
+
tokenEndpoint="/api/archiva/frontend-token"
|
|
94
|
+
projectId="proj_123" // Optional: for project-scoped tokens
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</ArchivaProvider>
|
|
98
|
+
</body>
|
|
99
|
+
</html>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
91
102
|
```
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
### 4. Use the Timeline Component
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
Now you can use the `Timeline` component in any client component. It will automatically:
|
|
107
|
+
- Fetch short-lived frontend tokens
|
|
108
|
+
- Call the Archiva API directly (no proxy routes needed)
|
|
109
|
+
- Handle token refresh automatically
|
|
110
|
+
- Retry on 401/403 errors
|
|
111
|
+
|
|
112
|
+
**File:** `app/invoice/[id]/page.tsx`
|
|
96
113
|
|
|
97
114
|
```tsx
|
|
98
|
-
|
|
115
|
+
'use client';
|
|
116
|
+
|
|
117
|
+
import { Timeline } from '@archiva/archiva-nextjs/react/client';
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
},
|
|
113
|
-
]);
|
|
119
|
+
export default function InvoicePage({ params }: { params: { id: string } }) {
|
|
120
|
+
return (
|
|
121
|
+
<div>
|
|
122
|
+
<h1>Invoice {params.id}</h1>
|
|
123
|
+
<Timeline
|
|
124
|
+
entityId={params.id}
|
|
125
|
+
entityType="invoice"
|
|
126
|
+
initialLimit={25}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
114
131
|
```
|
|
115
132
|
|
|
116
|
-
|
|
133
|
+
**Note:** Client components like `Timeline` and `useArchiva` must be imported from `@archiva/archiva-nextjs/react/client` to ensure proper client/server code splitting.
|
|
117
134
|
|
|
118
|
-
|
|
135
|
+
## How It Works
|
|
136
|
+
|
|
137
|
+
The SDK implements a Clerk-style provider pattern with short-lived frontend tokens:
|
|
138
|
+
|
|
139
|
+
1. **Server Component (`ArchivaProvider`)**: Validates `ARCHIVA_SECRET_KEY` exists (never exposes it to the client)
|
|
140
|
+
2. **Client Component (`ArchivaProviderClient`)**: Manages token lifecycle:
|
|
141
|
+
- Fetches tokens from your `/api/archiva/frontend-token` route
|
|
142
|
+
- Caches tokens in memory
|
|
143
|
+
- Auto-refreshes 30 seconds before expiry
|
|
144
|
+
- Handles 401/403 errors with automatic retry
|
|
145
|
+
3. **Timeline Component**: Uses tokens to call Archiva API directly
|
|
146
|
+
|
|
147
|
+
### Token Flow
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Client Component → useArchiva().getToken()
|
|
151
|
+
↓
|
|
152
|
+
ArchivaProviderClient checks cache
|
|
153
|
+
↓
|
|
154
|
+
If expired/absent → GET /api/archiva/frontend-token
|
|
155
|
+
↓
|
|
156
|
+
Your route → POST /v1/frontend-tokens (Archiva API)
|
|
157
|
+
↓
|
|
158
|
+
Server mints JWT (90s expiry)
|
|
159
|
+
↓
|
|
160
|
+
Token returned → Cached → Used for API calls
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Advanced Usage
|
|
164
|
+
|
|
165
|
+
### Using the `useArchiva` Hook
|
|
166
|
+
|
|
167
|
+
Access the Archiva context directly in your components:
|
|
119
168
|
|
|
120
169
|
```tsx
|
|
121
170
|
'use client';
|
|
122
171
|
|
|
123
|
-
import {
|
|
172
|
+
import { useArchiva } from '@archiva/archiva-nextjs/react/client';
|
|
173
|
+
|
|
174
|
+
export function CustomTimeline({ entityId }: { entityId: string }) {
|
|
175
|
+
const { apiBaseUrl, getToken, forceRefreshToken } = useArchiva();
|
|
176
|
+
const [events, setEvents] = React.useState([]);
|
|
177
|
+
|
|
178
|
+
React.useEffect(() => {
|
|
179
|
+
async function loadEvents() {
|
|
180
|
+
const token = await getToken();
|
|
181
|
+
|
|
182
|
+
const response = await fetch(`${apiBaseUrl}/api/events?entityId=${entityId}`, {
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `Bearer ${token}`,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
// SDK automatically handles 401/403 with retry
|
|
190
|
+
throw new Error('Failed to load events');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const data = await response.json();
|
|
194
|
+
setEvents(data.items);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
loadEvents();
|
|
198
|
+
}, [entityId, apiBaseUrl, getToken]);
|
|
124
199
|
|
|
125
|
-
export function InvoiceTimeline({ invoiceId }: { invoiceId: string }) {
|
|
126
200
|
return (
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
201
|
+
<div>
|
|
202
|
+
{events.map((event) => (
|
|
203
|
+
<div key={event.id}>{event.action}</div>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
132
206
|
);
|
|
133
207
|
}
|
|
134
208
|
```
|
|
135
209
|
|
|
136
|
-
|
|
210
|
+
### Manual Token Refresh
|
|
211
|
+
|
|
212
|
+
Force a token refresh if needed:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
'use client';
|
|
216
|
+
|
|
217
|
+
import { useArchiva } from '@archiva/archiva-nextjs/react/client';
|
|
218
|
+
|
|
219
|
+
export function RefreshButton() {
|
|
220
|
+
const { forceRefreshToken } = useArchiva();
|
|
221
|
+
|
|
222
|
+
const handleRefresh = async () => {
|
|
223
|
+
try {
|
|
224
|
+
const newToken = await forceRefreshToken();
|
|
225
|
+
console.log('Token refreshed:', newToken);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Failed to refresh token:', error);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return <button onClick={handleRefresh}>Refresh Token</button>;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Project-Scoped Tokens
|
|
236
|
+
|
|
237
|
+
If you want tokens scoped to a specific project, pass `projectId` to the provider:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
<ArchivaProvider projectId="proj_123">
|
|
241
|
+
{children}
|
|
242
|
+
</ArchivaProvider>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Or pass it as a query parameter to your token endpoint:
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
// GET /api/archiva/frontend-token?projectId=proj_123
|
|
249
|
+
```
|
|
137
250
|
|
|
138
251
|
## API Reference
|
|
139
252
|
|
|
140
|
-
###
|
|
253
|
+
### ArchivaProvider Props
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
type ArchivaProviderProps = {
|
|
257
|
+
children: ReactNode;
|
|
258
|
+
apiBaseUrl?: string; // Default: 'https://api.archiva.app'
|
|
259
|
+
tokenEndpoint?: string; // Default: '/api/archiva/frontend-token'
|
|
260
|
+
projectId?: string; // Optional: for project-scoped tokens
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Timeline Props
|
|
141
265
|
|
|
142
266
|
```ts
|
|
143
|
-
type
|
|
267
|
+
type TimelineProps = {
|
|
144
268
|
entityId?: string;
|
|
145
269
|
actorId?: string;
|
|
146
270
|
entityType?: string;
|
|
147
|
-
|
|
148
|
-
|
|
271
|
+
initialLimit?: number; // Default: 25
|
|
272
|
+
className?: string;
|
|
273
|
+
emptyMessage?: string; // Default: 'No events yet.'
|
|
149
274
|
};
|
|
150
275
|
```
|
|
151
276
|
|
|
152
|
-
###
|
|
277
|
+
### useArchiva Hook
|
|
153
278
|
|
|
154
279
|
```ts
|
|
155
|
-
type
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
actorId?: string;
|
|
161
|
-
actorDisplay?: string;
|
|
162
|
-
occurredAt?: string;
|
|
163
|
-
source?: string;
|
|
164
|
-
context?: Record<string, unknown>;
|
|
165
|
-
changes?: EventChange[];
|
|
280
|
+
type ArchivaContextValue = {
|
|
281
|
+
apiBaseUrl: string;
|
|
282
|
+
getToken: () => Promise<string>;
|
|
283
|
+
forceRefreshToken: () => Promise<string>;
|
|
284
|
+
projectId?: string;
|
|
166
285
|
};
|
|
167
286
|
```
|
|
168
287
|
|
|
288
|
+
## Security
|
|
289
|
+
|
|
290
|
+
- **No secrets in client bundles**: `ARCHIVA_SECRET_KEY` is only used on the server
|
|
291
|
+
- **Short-lived tokens**: Tokens expire in 90 seconds
|
|
292
|
+
- **Automatic refresh**: Tokens refresh 30 seconds before expiry
|
|
293
|
+
- **Scope enforcement**: Tokens require `timeline:read` scope
|
|
294
|
+
- **Project scoping**: Tokens can be scoped to specific projects
|
|
295
|
+
|
|
296
|
+
## Import Paths
|
|
297
|
+
|
|
298
|
+
The SDK provides explicit entrypoints to ensure proper server/client code splitting:
|
|
299
|
+
|
|
300
|
+
### Server Components (e.g., `app/layout.tsx`)
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
// ✅ Correct - Server-safe import
|
|
304
|
+
import { ArchivaProvider } from '@archiva/archiva-nextjs/react';
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Client Components
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
'use client';
|
|
311
|
+
|
|
312
|
+
// ✅ Correct - Client-only imports
|
|
313
|
+
import { Timeline, useArchiva } from '@archiva/archiva-nextjs/react/client';
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Server Routes (e.g., `app/api/archiva/frontend-token/route.ts`)
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
// ✅ Correct - Server utilities
|
|
320
|
+
import { GET } from '@archiva/archiva-nextjs/server';
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Why Separate Entrypoints?
|
|
324
|
+
|
|
325
|
+
Next.js App Router requires strict separation between server and client code. The SDK splits exports to prevent RSC errors:
|
|
326
|
+
|
|
327
|
+
- `@archiva/archiva-nextjs/react` - Server-safe exports (ArchivaProvider only)
|
|
328
|
+
- `@archiva/archiva-nextjs/react/client` - Client-only exports (Timeline, useArchiva)
|
|
329
|
+
- `@archiva/archiva-nextjs/server` - Server utilities (route handlers)
|
|
330
|
+
|
|
331
|
+
**Important:** Do NOT import client components from the root package or from `/react` in Server Components, as this will trigger RSC errors.
|
|
332
|
+
|
|
333
|
+
## Backward Compatibility
|
|
334
|
+
|
|
335
|
+
Legacy exports are still available from the root package for backward compatibility, but they are deprecated:
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
// ⚠️ Legacy (deprecated - may cause RSC errors in Server Components)
|
|
339
|
+
import { ArchivaProvider, Timeline } from '@archiva/archiva-nextjs';
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
For new projects, always use the explicit entrypoints shown above.
|
|
343
|
+
|
|
344
|
+
## Troubleshooting
|
|
345
|
+
|
|
346
|
+
### "ARCHIVA_SECRET_KEY not configured"
|
|
347
|
+
|
|
348
|
+
Make sure you've set `ARCHIVA_SECRET_KEY` in your `.env.local` file and restarted your Next.js dev server.
|
|
349
|
+
|
|
350
|
+
### "useArchivaContext must be used within an ArchivaProvider"
|
|
351
|
+
|
|
352
|
+
Wrap your component tree with `<ArchivaProvider>`. The provider must be a server component in your layout.
|
|
353
|
+
|
|
354
|
+
### Token endpoint returns 401
|
|
355
|
+
|
|
356
|
+
Verify that:
|
|
357
|
+
1. `ARCHIVA_SECRET_KEY` is set correctly
|
|
358
|
+
2. The API key is valid and active
|
|
359
|
+
3. Your token endpoint route is correctly set up
|
|
360
|
+
|
|
361
|
+
### Timeline shows "Error: Failed to fetch frontend token"
|
|
362
|
+
|
|
363
|
+
Check that:
|
|
364
|
+
1. Your `/api/archiva/frontend-token` route exists
|
|
365
|
+
2. The route handler is exported correctly
|
|
366
|
+
3. `ARCHIVA_SECRET_KEY` is accessible to the route handler
|
|
367
|
+
|
|
169
368
|
## License
|
|
170
369
|
|
|
171
370
|
MIT
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArchivaContext
|
|
3
|
+
} from "./chunk-XZUDW4PU.mjs";
|
|
4
|
+
|
|
5
|
+
// src/react/ArchivaProvider.tsx
|
|
6
|
+
import "server-only";
|
|
7
|
+
|
|
8
|
+
// src/react/internal/ArchivaProviderClient.tsx
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { jsx } from "react/jsx-runtime";
|
|
11
|
+
function ArchivaProviderClient({
|
|
12
|
+
children,
|
|
13
|
+
apiBaseUrl,
|
|
14
|
+
tokenEndpoint,
|
|
15
|
+
projectId
|
|
16
|
+
}) {
|
|
17
|
+
const [tokenCache, setTokenCache] = React.useState(null);
|
|
18
|
+
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
|
19
|
+
const refreshTimeoutRef = React.useRef(null);
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
return () => {
|
|
22
|
+
if (refreshTimeoutRef.current) {
|
|
23
|
+
clearTimeout(refreshTimeoutRef.current);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
const fetchToken = React.useCallback(async () => {
|
|
28
|
+
const baseUrl = tokenEndpoint.startsWith("http") ? tokenEndpoint : typeof window !== "undefined" ? `${window.location.origin}${tokenEndpoint}` : `${apiBaseUrl}${tokenEndpoint}`;
|
|
29
|
+
const url = new URL(baseUrl);
|
|
30
|
+
if (projectId) {
|
|
31
|
+
url.searchParams.set("projectId", projectId);
|
|
32
|
+
}
|
|
33
|
+
const response = await fetch(url.toString(), {
|
|
34
|
+
method: "GET",
|
|
35
|
+
credentials: "include"
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error = await response.json().catch(() => ({ error: "Failed to fetch token" }));
|
|
39
|
+
throw new Error(error.error || "Failed to fetch frontend token");
|
|
40
|
+
}
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
if (!data.token || !data.expiresAt) {
|
|
43
|
+
throw new Error("Invalid token response");
|
|
44
|
+
}
|
|
45
|
+
return data.token;
|
|
46
|
+
}, [tokenEndpoint, projectId, apiBaseUrl]);
|
|
47
|
+
const getToken = React.useCallback(async () => {
|
|
48
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
49
|
+
if (tokenCache && tokenCache.expiresAt > now + 30) {
|
|
50
|
+
return tokenCache.token;
|
|
51
|
+
}
|
|
52
|
+
if (isRefreshing) {
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
54
|
+
return getToken();
|
|
55
|
+
}
|
|
56
|
+
setIsRefreshing(true);
|
|
57
|
+
try {
|
|
58
|
+
const token = await fetchToken();
|
|
59
|
+
const expiresAt = getTokenExpiry(token);
|
|
60
|
+
if (!expiresAt) {
|
|
61
|
+
throw new Error("Failed to parse token expiry");
|
|
62
|
+
}
|
|
63
|
+
const newCache = { token, expiresAt };
|
|
64
|
+
setTokenCache(newCache);
|
|
65
|
+
const refreshIn = Math.max(0, expiresAt - now - 30) * 1e3;
|
|
66
|
+
if (refreshTimeoutRef.current) {
|
|
67
|
+
clearTimeout(refreshTimeoutRef.current);
|
|
68
|
+
}
|
|
69
|
+
refreshTimeoutRef.current = setTimeout(() => {
|
|
70
|
+
setTokenCache(null);
|
|
71
|
+
}, refreshIn);
|
|
72
|
+
return token;
|
|
73
|
+
} finally {
|
|
74
|
+
setIsRefreshing(false);
|
|
75
|
+
}
|
|
76
|
+
}, [tokenCache, isRefreshing, fetchToken]);
|
|
77
|
+
const forceRefreshToken = React.useCallback(async () => {
|
|
78
|
+
setTokenCache(null);
|
|
79
|
+
if (refreshTimeoutRef.current) {
|
|
80
|
+
clearTimeout(refreshTimeoutRef.current);
|
|
81
|
+
refreshTimeoutRef.current = null;
|
|
82
|
+
}
|
|
83
|
+
setIsRefreshing(true);
|
|
84
|
+
try {
|
|
85
|
+
const token = await fetchToken();
|
|
86
|
+
const expiresAt = getTokenExpiry(token);
|
|
87
|
+
if (!expiresAt) {
|
|
88
|
+
throw new Error("Failed to parse token expiry");
|
|
89
|
+
}
|
|
90
|
+
const newCache = { token, expiresAt };
|
|
91
|
+
setTokenCache(newCache);
|
|
92
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
93
|
+
const refreshIn = Math.max(0, expiresAt - now - 30) * 1e3;
|
|
94
|
+
if (refreshTimeoutRef.current) {
|
|
95
|
+
clearTimeout(refreshTimeoutRef.current);
|
|
96
|
+
}
|
|
97
|
+
refreshTimeoutRef.current = setTimeout(() => {
|
|
98
|
+
setTokenCache(null);
|
|
99
|
+
}, refreshIn);
|
|
100
|
+
return token;
|
|
101
|
+
} finally {
|
|
102
|
+
setIsRefreshing(false);
|
|
103
|
+
}
|
|
104
|
+
}, [fetchToken]);
|
|
105
|
+
const value = React.useMemo(
|
|
106
|
+
() => ({
|
|
107
|
+
apiBaseUrl,
|
|
108
|
+
getToken,
|
|
109
|
+
forceRefreshToken,
|
|
110
|
+
projectId
|
|
111
|
+
}),
|
|
112
|
+
[apiBaseUrl, getToken, forceRefreshToken, projectId]
|
|
113
|
+
);
|
|
114
|
+
return /* @__PURE__ */ jsx(ArchivaContext.Provider, { value, children });
|
|
115
|
+
}
|
|
116
|
+
function getTokenExpiry(token) {
|
|
117
|
+
try {
|
|
118
|
+
const parts = token.split(".");
|
|
119
|
+
if (parts.length !== 3) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const payload = JSON.parse(
|
|
123
|
+
atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"))
|
|
124
|
+
);
|
|
125
|
+
return payload.exp;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/react/ArchivaProvider.tsx
|
|
132
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
133
|
+
function ArchivaProvider({
|
|
134
|
+
children,
|
|
135
|
+
apiBaseUrl = "https://api.archiva.app",
|
|
136
|
+
tokenEndpoint = "/api/archiva/frontend-token",
|
|
137
|
+
projectId
|
|
138
|
+
}) {
|
|
139
|
+
if (!process.env.ARCHIVA_SECRET_KEY) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"ARCHIVA_SECRET_KEY environment variable is required. Set it in your .env.local file."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return /* @__PURE__ */ jsx2(
|
|
145
|
+
ArchivaProviderClient,
|
|
146
|
+
{
|
|
147
|
+
apiBaseUrl,
|
|
148
|
+
tokenEndpoint,
|
|
149
|
+
projectId,
|
|
150
|
+
children
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
ArchivaProvider
|
|
157
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/react/context.tsx
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
var ArchivaContext = React.createContext(void 0);
|
|
4
|
+
function useArchivaContext() {
|
|
5
|
+
const context = React.useContext(ArchivaContext);
|
|
6
|
+
if (context === void 0) {
|
|
7
|
+
throw new Error("useArchivaContext must be used within an ArchivaProvider");
|
|
8
|
+
}
|
|
9
|
+
return context;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
ArchivaContext,
|
|
14
|
+
useArchivaContext
|
|
15
|
+
};
|