@zea.cl/auth 0.1.6
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/bin/zea-auth-init.mjs +53 -0
- package/dist/components/index.d.mts +150 -0
- package/dist/components/index.d.ts +150 -0
- package/dist/components/index.js +957 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +947 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/hooks/index.d.mts +2 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +621 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +618 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/index-BnHWPrKX.d.mts +69 -0
- package/dist/index-BnHWPrKX.d.ts +69 -0
- package/dist/index-C5rsqdqK.d.ts +453 -0
- package/dist/index-DtXFjTm2.d.mts +453 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1041 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1024 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles/base.css +219 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
7
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
8
|
+
}) : x)(function(x) {
|
|
9
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
10
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// src/client/auth/OAuth2.ts
|
|
14
|
+
var OAuth2 = class {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
config;
|
|
19
|
+
/**
|
|
20
|
+
* Generate OAuth2 authorization URL for user login
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const authUrl = thalamus.auth.getAuthorizationUrl({
|
|
25
|
+
* scope: ['openid', 'profile', 'email'],
|
|
26
|
+
* state: 'random-state-string'
|
|
27
|
+
* })
|
|
28
|
+
* // Redirect user to authUrl
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
getAuthorizationUrl(options) {
|
|
32
|
+
const {
|
|
33
|
+
scope = this.config.defaultScopes || ["openid", "profile", "email"],
|
|
34
|
+
state = this.generateState(),
|
|
35
|
+
responseType = "code",
|
|
36
|
+
codeChallenge,
|
|
37
|
+
codeChallengeMethod
|
|
38
|
+
} = options || {};
|
|
39
|
+
const params = new URLSearchParams({
|
|
40
|
+
response_type: responseType,
|
|
41
|
+
client_id: this.config.clientId,
|
|
42
|
+
redirect_uri: this.config.redirectUri,
|
|
43
|
+
scope: Array.isArray(scope) ? scope.join(" ") : scope,
|
|
44
|
+
state
|
|
45
|
+
});
|
|
46
|
+
if (codeChallenge) {
|
|
47
|
+
params.set("code_challenge", codeChallenge);
|
|
48
|
+
params.set("code_challenge_method", codeChallengeMethod || "S256");
|
|
49
|
+
}
|
|
50
|
+
return `${this.config.baseUrl}/oauth/authorize?${params.toString()}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Exchange authorization code for access token
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const tokens = await thalamus.auth.exchangeCode('authorization_code_here')
|
|
58
|
+
* console.log(tokens.access_token)
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async exchangeCode(codeOrOptions) {
|
|
62
|
+
const code = typeof codeOrOptions === "string" ? codeOrOptions : codeOrOptions.code;
|
|
63
|
+
const codeVerifier = typeof codeOrOptions === "string" ? void 0 : codeOrOptions.codeVerifier;
|
|
64
|
+
const body = {
|
|
65
|
+
grant_type: "authorization_code",
|
|
66
|
+
code,
|
|
67
|
+
client_id: this.config.clientId,
|
|
68
|
+
client_secret: this.config.clientSecret,
|
|
69
|
+
redirect_uri: this.config.redirectUri
|
|
70
|
+
};
|
|
71
|
+
if (codeVerifier) {
|
|
72
|
+
body.code_verifier = codeVerifier;
|
|
73
|
+
}
|
|
74
|
+
return this.requestToken(body);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get access token using client credentials (M2M)
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const tokens = await thalamus.auth.getClientCredentialsToken({
|
|
82
|
+
* scope: ['api:read', 'api:write']
|
|
83
|
+
* })
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
async getClientCredentialsToken(options) {
|
|
87
|
+
const { scope = this.config.defaultScopes || [] } = options || {};
|
|
88
|
+
const body = {
|
|
89
|
+
grant_type: "client_credentials",
|
|
90
|
+
client_id: this.config.clientId,
|
|
91
|
+
client_secret: this.config.clientSecret
|
|
92
|
+
};
|
|
93
|
+
if (scope.length > 0) {
|
|
94
|
+
body.scope = Array.isArray(scope) ? scope.join(" ") : scope;
|
|
95
|
+
}
|
|
96
|
+
return this.requestToken(body);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Refresh access token using refresh token
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const newTokens = await thalamus.auth.refreshToken({
|
|
104
|
+
* refreshToken: 'rt_...'
|
|
105
|
+
* })
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
async refreshToken(options) {
|
|
109
|
+
const body = {
|
|
110
|
+
grant_type: "refresh_token",
|
|
111
|
+
refresh_token: options.refreshToken,
|
|
112
|
+
client_id: this.config.clientId,
|
|
113
|
+
client_secret: this.config.clientSecret
|
|
114
|
+
};
|
|
115
|
+
return this.requestToken(body);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Revoke a token (access or refresh token)
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* await thalamus.auth.revokeToken('at_...')
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
async revokeToken(token, tokenTypeHint) {
|
|
126
|
+
const body = {
|
|
127
|
+
token
|
|
128
|
+
};
|
|
129
|
+
if (tokenTypeHint) {
|
|
130
|
+
body.token_type_hint = tokenTypeHint;
|
|
131
|
+
}
|
|
132
|
+
const response = await fetch(`${this.config.baseUrl}/oauth/revoke`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
"Content-Type": "application/json"
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(body)
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw await this.handleError(response);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Generate random state for CSRF protection
|
|
145
|
+
*/
|
|
146
|
+
generateState(length = 32) {
|
|
147
|
+
const array = new Uint8Array(32);
|
|
148
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
149
|
+
crypto.getRandomValues(array);
|
|
150
|
+
} else {
|
|
151
|
+
const cryptoModule = __require("crypto");
|
|
152
|
+
cryptoModule.randomFillSync(array);
|
|
153
|
+
}
|
|
154
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Make token request to /oauth/token
|
|
158
|
+
*/
|
|
159
|
+
async requestToken(body) {
|
|
160
|
+
const response = await fetch(`${this.config.baseUrl}/oauth/token`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: {
|
|
163
|
+
"Content-Type": "application/json"
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify(body)
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw await this.handleError(response);
|
|
169
|
+
}
|
|
170
|
+
return response.json();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Handle API errors
|
|
174
|
+
*/
|
|
175
|
+
async handleError(response) {
|
|
176
|
+
let errorData = {};
|
|
177
|
+
try {
|
|
178
|
+
errorData = await response.json();
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
const error = new Error(
|
|
182
|
+
errorData.error_description || errorData.message || `HTTP ${response.status}`
|
|
183
|
+
);
|
|
184
|
+
error.statusCode = response.status;
|
|
185
|
+
error.error = errorData.error;
|
|
186
|
+
error.error_description = errorData.error_description;
|
|
187
|
+
return error;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/client/tokens/TokenManager.ts
|
|
192
|
+
var TokenManager = class {
|
|
193
|
+
constructor(config) {
|
|
194
|
+
this.config = config;
|
|
195
|
+
}
|
|
196
|
+
config;
|
|
197
|
+
/**
|
|
198
|
+
* Introspect a token to check if it's valid and get metadata
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* const tokenInfo = await thalamus.tokens.introspect('at_...')
|
|
203
|
+
* if (tokenInfo.active) {
|
|
204
|
+
* console.log(tokenInfo.user_id)
|
|
205
|
+
* console.log(tokenInfo.scope)
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
async introspect(token) {
|
|
210
|
+
const response = await fetch(`${this.config.baseUrl}/oauth/introspect`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": "application/json"
|
|
214
|
+
},
|
|
215
|
+
body: JSON.stringify({ token })
|
|
216
|
+
});
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
throw await this.handleError(response);
|
|
219
|
+
}
|
|
220
|
+
return response.json();
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get user information from OpenID Connect userinfo endpoint
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* const user = await thalamus.tokens.getUserInfo('at_...')
|
|
228
|
+
* console.log(user.email)
|
|
229
|
+
* console.log(user.name)
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
async getUserInfo(accessToken) {
|
|
233
|
+
const response = await fetch(`${this.config.baseUrl}/oauth/userinfo`, {
|
|
234
|
+
method: "GET",
|
|
235
|
+
headers: {
|
|
236
|
+
Authorization: `Bearer ${accessToken}`
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw await this.handleError(response);
|
|
241
|
+
}
|
|
242
|
+
return response.json();
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validate token and return true if active, false otherwise
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* const isValid = await thalamus.tokens.validate('at_...')
|
|
250
|
+
* if (isValid) {
|
|
251
|
+
* // Token is valid
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
async validate(token) {
|
|
256
|
+
try {
|
|
257
|
+
const result = await this.introspect(token);
|
|
258
|
+
return result.active === true;
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Handle API errors
|
|
265
|
+
*/
|
|
266
|
+
async handleError(response) {
|
|
267
|
+
let errorData = {};
|
|
268
|
+
try {
|
|
269
|
+
errorData = await response.json();
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
const error = new Error(
|
|
273
|
+
errorData.error_description || errorData.message || `HTTP ${response.status}`
|
|
274
|
+
);
|
|
275
|
+
error.statusCode = response.status;
|
|
276
|
+
error.error = errorData.error;
|
|
277
|
+
error.error_description = errorData.error_description;
|
|
278
|
+
return error;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/client/admin/AdminAPI.ts
|
|
283
|
+
var AdminAPI = class {
|
|
284
|
+
constructor(config) {
|
|
285
|
+
this.config = config;
|
|
286
|
+
}
|
|
287
|
+
config;
|
|
288
|
+
get baseUrl() {
|
|
289
|
+
return this.config.baseUrl;
|
|
290
|
+
}
|
|
291
|
+
// ── Users ────────────────────────────────────────────────────────────────
|
|
292
|
+
/** List all users */
|
|
293
|
+
async listUsers() {
|
|
294
|
+
const res = await this.request(`${this.baseUrl}/api/users`);
|
|
295
|
+
const json = await res.json();
|
|
296
|
+
return json.data ?? json;
|
|
297
|
+
}
|
|
298
|
+
/** List all agents (users with is_agent === true) */
|
|
299
|
+
async listAgents() {
|
|
300
|
+
const users = await this.listUsers();
|
|
301
|
+
return users.filter((u) => u.is_agent === true);
|
|
302
|
+
}
|
|
303
|
+
/** Get a single user */
|
|
304
|
+
async getUser(id) {
|
|
305
|
+
const res = await this.request(`${this.baseUrl}/api/users/${id}`);
|
|
306
|
+
const json = await res.json();
|
|
307
|
+
return json.data ?? json;
|
|
308
|
+
}
|
|
309
|
+
/** Add a member to an organization */
|
|
310
|
+
async addOrgMember(orgId, userId) {
|
|
311
|
+
const res = await this.request(`${this.baseUrl}/api/organizations/${orgId}/members`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
body: JSON.stringify({ user_id: userId })
|
|
314
|
+
});
|
|
315
|
+
return res.json();
|
|
316
|
+
}
|
|
317
|
+
/** Update a user (only name and agent_config are writable) */
|
|
318
|
+
async updateUser(id, data) {
|
|
319
|
+
const res = await this.request(`${this.baseUrl}/api/users/${id}`, {
|
|
320
|
+
method: "PATCH",
|
|
321
|
+
body: JSON.stringify({ user: data })
|
|
322
|
+
});
|
|
323
|
+
const json = await res.json();
|
|
324
|
+
return json.data ?? json;
|
|
325
|
+
}
|
|
326
|
+
/** Create a user */
|
|
327
|
+
async createUser(data) {
|
|
328
|
+
const res = await this.request(`${this.baseUrl}/api/users`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
body: JSON.stringify({ user: data })
|
|
331
|
+
});
|
|
332
|
+
const json = await res.json();
|
|
333
|
+
return json.data ?? json;
|
|
334
|
+
}
|
|
335
|
+
// ── Organizations ─────────────────────────────────────────────────────────
|
|
336
|
+
/** Get an organization */
|
|
337
|
+
async getOrganization(id) {
|
|
338
|
+
const res = await this.request(`${this.baseUrl}/api/organizations/${id}`);
|
|
339
|
+
const json = await res.json();
|
|
340
|
+
return json.data ?? json;
|
|
341
|
+
}
|
|
342
|
+
/** List all organizations */
|
|
343
|
+
async listOrganizations() {
|
|
344
|
+
const res = await this.request(`${this.baseUrl}/api/organizations`);
|
|
345
|
+
const json = await res.json();
|
|
346
|
+
return json.data ?? json;
|
|
347
|
+
}
|
|
348
|
+
// ── Domain Roles ──────────────────────────────────────────────────────────
|
|
349
|
+
/** List domain roles (optionally filtered) */
|
|
350
|
+
async listDomainRoles(filters) {
|
|
351
|
+
const params = new URLSearchParams();
|
|
352
|
+
if (filters?.user_id) params.set("user_id", filters.user_id);
|
|
353
|
+
if (filters?.organization_id) params.set("organization_id", filters.organization_id);
|
|
354
|
+
if (filters?.domain) params.set("domain", filters.domain);
|
|
355
|
+
const qs = params.toString();
|
|
356
|
+
const url = `${this.baseUrl}/api/domains/roles${qs ? `?${qs}` : ""}`;
|
|
357
|
+
const res = await this.request(url);
|
|
358
|
+
const json = await res.json();
|
|
359
|
+
return json.data ?? json;
|
|
360
|
+
}
|
|
361
|
+
/** Grant a domain role to a user */
|
|
362
|
+
async grantDomainRole(params) {
|
|
363
|
+
const res = await this.request(`${this.baseUrl}/api/domains/roles/grant`, {
|
|
364
|
+
method: "POST",
|
|
365
|
+
body: JSON.stringify(params)
|
|
366
|
+
});
|
|
367
|
+
return res.json();
|
|
368
|
+
}
|
|
369
|
+
/** Revoke a domain role from a user */
|
|
370
|
+
async revokeDomainRole(params) {
|
|
371
|
+
const res = await this.request(`${this.baseUrl}/api/domains/roles/revoke`, {
|
|
372
|
+
method: "DELETE",
|
|
373
|
+
body: JSON.stringify(params)
|
|
374
|
+
});
|
|
375
|
+
return res.json();
|
|
376
|
+
}
|
|
377
|
+
// ── Roles (RBAC) ──────────────────────────────────────────────────────────
|
|
378
|
+
/** List all roles */
|
|
379
|
+
async listRoles() {
|
|
380
|
+
const res = await this.request(`${this.baseUrl}/api/roles`);
|
|
381
|
+
const json = await res.json();
|
|
382
|
+
return json.data ?? json;
|
|
383
|
+
}
|
|
384
|
+
/** Create a role */
|
|
385
|
+
async createRole(params) {
|
|
386
|
+
const res = await this.request(`${this.baseUrl}/api/roles`, {
|
|
387
|
+
method: "POST",
|
|
388
|
+
body: JSON.stringify(params)
|
|
389
|
+
});
|
|
390
|
+
const json = await res.json();
|
|
391
|
+
return json.data ?? json;
|
|
392
|
+
}
|
|
393
|
+
/** Delete a role */
|
|
394
|
+
async deleteRole(id) {
|
|
395
|
+
await this.request(`${this.baseUrl}/api/roles/${id}`, { method: "DELETE" });
|
|
396
|
+
}
|
|
397
|
+
// ── User Roles ────────────────────────────────────────────────────────────
|
|
398
|
+
/** Get user's effective scopes */
|
|
399
|
+
async getEffectiveScopes(userId) {
|
|
400
|
+
const res = await this.request(`${this.baseUrl}/api/users/${userId}/effective-scopes`);
|
|
401
|
+
const json = await res.json();
|
|
402
|
+
return json.data ?? json;
|
|
403
|
+
}
|
|
404
|
+
// ── Internal ──────────────────────────────────────────────────────────────
|
|
405
|
+
getAccessToken() {
|
|
406
|
+
if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
|
|
407
|
+
const storageKey = this.config.storageKey || "thalamus_auth";
|
|
408
|
+
const saved = globalThis.localStorage.getItem(storageKey);
|
|
409
|
+
if (saved) {
|
|
410
|
+
try {
|
|
411
|
+
return JSON.parse(saved).accessToken;
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
async request(url, options = {}) {
|
|
420
|
+
const token = this.getAccessToken();
|
|
421
|
+
const res = await fetch(url, {
|
|
422
|
+
...options,
|
|
423
|
+
headers: {
|
|
424
|
+
"Content-Type": "application/json",
|
|
425
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
426
|
+
...options.headers
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
throw await this.toError(res);
|
|
431
|
+
}
|
|
432
|
+
return res;
|
|
433
|
+
}
|
|
434
|
+
async toError(response) {
|
|
435
|
+
let body = {};
|
|
436
|
+
try {
|
|
437
|
+
body = await response.json();
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
const error = new Error(
|
|
441
|
+
body.error_description || body.error || `HTTP ${response.status}`
|
|
442
|
+
);
|
|
443
|
+
error.statusCode = response.status;
|
|
444
|
+
error.error = body.error;
|
|
445
|
+
error.error_description = body.error_description;
|
|
446
|
+
return error;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/client/ThalamusClient.ts
|
|
451
|
+
var ThalamusClient = class {
|
|
452
|
+
/** OAuth2 authentication methods */
|
|
453
|
+
auth;
|
|
454
|
+
/** Token management and introspection */
|
|
455
|
+
tokens;
|
|
456
|
+
/** Admin API — users, orgs, roles, domain management */
|
|
457
|
+
admin;
|
|
458
|
+
config;
|
|
459
|
+
/**
|
|
460
|
+
* Create a new Thalamus client
|
|
461
|
+
*
|
|
462
|
+
* @param config - Client configuration
|
|
463
|
+
*/
|
|
464
|
+
constructor(config) {
|
|
465
|
+
if (!config.clientId) {
|
|
466
|
+
throw new Error("clientId is required");
|
|
467
|
+
}
|
|
468
|
+
if (!config.redirectUri) {
|
|
469
|
+
throw new Error("redirectUri is required");
|
|
470
|
+
}
|
|
471
|
+
if (!config.baseUrl) {
|
|
472
|
+
throw new Error("baseUrl is required");
|
|
473
|
+
}
|
|
474
|
+
config.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
475
|
+
this.config = config;
|
|
476
|
+
this.auth = new OAuth2(config);
|
|
477
|
+
this.tokens = new TokenManager(config);
|
|
478
|
+
this.admin = new AdminAPI(config);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get the current configuration
|
|
482
|
+
*/
|
|
483
|
+
getConfig() {
|
|
484
|
+
return Object.freeze({ ...this.config });
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/hooks/useThalamus.ts
|
|
489
|
+
function useThalamus(options) {
|
|
490
|
+
const storageKey = options.storageKey || "thalamus_auth";
|
|
491
|
+
const clientRef = react.useRef(new ThalamusClient(options));
|
|
492
|
+
const client = clientRef.current;
|
|
493
|
+
const [token, setToken] = react.useState(null);
|
|
494
|
+
const [user, setUser] = react.useState(null);
|
|
495
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
496
|
+
const [error, setError] = react.useState(null);
|
|
497
|
+
react.useEffect(() => {
|
|
498
|
+
try {
|
|
499
|
+
const saved = localStorage.getItem(storageKey);
|
|
500
|
+
if (saved) {
|
|
501
|
+
const { accessToken, refreshToken: rt } = JSON.parse(saved);
|
|
502
|
+
setToken(accessToken);
|
|
503
|
+
client.tokens.getUserInfo(accessToken).then((u) => setUser(u)).catch(() => {
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
} finally {
|
|
508
|
+
setIsLoading(false);
|
|
509
|
+
}
|
|
510
|
+
}, [storageKey]);
|
|
511
|
+
react.useEffect(() => {
|
|
512
|
+
const params = new URLSearchParams(window.location.search);
|
|
513
|
+
const code = params.get("code");
|
|
514
|
+
const state = params.get("state");
|
|
515
|
+
if (!code || !state) return;
|
|
516
|
+
window.history.replaceState({}, "", window.location.pathname);
|
|
517
|
+
const savedState = sessionStorage.getItem(`${storageKey}_state`);
|
|
518
|
+
if (state !== savedState) {
|
|
519
|
+
setError("State mismatch \u2014 possible CSRF attack");
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const verifier = sessionStorage.getItem(`${storageKey}_verifier`);
|
|
523
|
+
if (!verifier) {
|
|
524
|
+
setError("Missing code verifier");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
client.auth.exchangeCode({ code, codeVerifier: verifier }).then((data) => {
|
|
528
|
+
persistAuth(data);
|
|
529
|
+
setToken(data.access_token);
|
|
530
|
+
client.tokens.getUserInfo(data.access_token).then((u) => setUser(u)).catch(() => {
|
|
531
|
+
});
|
|
532
|
+
}).catch((err) => setError(err.message));
|
|
533
|
+
}, [storageKey]);
|
|
534
|
+
const persistAuth = (data) => {
|
|
535
|
+
localStorage.setItem(storageKey, JSON.stringify({ accessToken: data.access_token, refreshToken: data.refresh_token || null, expiresAt: Date.now() + data.expires_in * 1e3 }));
|
|
536
|
+
};
|
|
537
|
+
const login = react.useCallback(async (opts) => {
|
|
538
|
+
setError(null);
|
|
539
|
+
const verifier = client.auth.generateState();
|
|
540
|
+
const challenge = await pkceChallenge(verifier);
|
|
541
|
+
sessionStorage.setItem(`${storageKey}_verifier`, verifier);
|
|
542
|
+
const url = client.auth.getAuthorizationUrl({ scope: opts?.scope, codeChallenge: challenge, codeChallengeMethod: "S256" });
|
|
543
|
+
sessionStorage.setItem(`${storageKey}_state`, new URL(url).searchParams.get("state") || "");
|
|
544
|
+
window.location.href = url;
|
|
545
|
+
}, [storageKey]);
|
|
546
|
+
const logout = react.useCallback(() => {
|
|
547
|
+
localStorage.removeItem(storageKey);
|
|
548
|
+
setToken(null);
|
|
549
|
+
setUser(null);
|
|
550
|
+
}, [storageKey]);
|
|
551
|
+
const refreshToken = react.useCallback(async () => {
|
|
552
|
+
try {
|
|
553
|
+
const saved = localStorage.getItem(storageKey);
|
|
554
|
+
if (!saved) return;
|
|
555
|
+
const { refreshToken: rt } = JSON.parse(saved);
|
|
556
|
+
if (!rt) return;
|
|
557
|
+
const data = await client.auth.refreshToken({ refreshToken: rt });
|
|
558
|
+
persistAuth(data);
|
|
559
|
+
setToken(data.access_token);
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
}, [storageKey]);
|
|
563
|
+
return {
|
|
564
|
+
login,
|
|
565
|
+
logout,
|
|
566
|
+
token,
|
|
567
|
+
user,
|
|
568
|
+
isAuthenticated: !!token,
|
|
569
|
+
isLoading,
|
|
570
|
+
refreshToken,
|
|
571
|
+
client,
|
|
572
|
+
error
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
async function pkceChallenge(verifier) {
|
|
576
|
+
const encoder = new TextEncoder();
|
|
577
|
+
const hash = await crypto.subtle.digest("SHA-256", encoder.encode(verifier));
|
|
578
|
+
return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
579
|
+
}
|
|
580
|
+
function useAdmin(options) {
|
|
581
|
+
const { baseUrl } = options;
|
|
582
|
+
const clientRef = react.useRef(new ThalamusClient({ clientId: "admin", redirectUri: typeof window !== "undefined" ? window.location.origin : "http://localhost", baseUrl }));
|
|
583
|
+
const [users, setUsers] = react.useState([]);
|
|
584
|
+
const [loading, setLoading] = react.useState(false);
|
|
585
|
+
const [error, setError] = react.useState(null);
|
|
586
|
+
const refresh = async () => {
|
|
587
|
+
setLoading(true);
|
|
588
|
+
setError(null);
|
|
589
|
+
try {
|
|
590
|
+
const u = await clientRef.current.admin.listUsers();
|
|
591
|
+
setUsers(u);
|
|
592
|
+
} catch (e) {
|
|
593
|
+
setError(e.message);
|
|
594
|
+
}
|
|
595
|
+
setLoading(false);
|
|
596
|
+
};
|
|
597
|
+
react.useEffect(() => {
|
|
598
|
+
if (options.autoFetch !== false) refresh();
|
|
599
|
+
}, [baseUrl]);
|
|
600
|
+
const agents = users.filter((u) => u.is_agent);
|
|
601
|
+
const organizations = [];
|
|
602
|
+
const roles = [];
|
|
603
|
+
const createUser = async (data) => {
|
|
604
|
+
try {
|
|
605
|
+
const u = await clientRef.current.admin.createUser(data);
|
|
606
|
+
setUsers((prev) => [...prev, u]);
|
|
607
|
+
return u;
|
|
608
|
+
} catch (e) {
|
|
609
|
+
setError(e.message);
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
const listDomainRoles = async (filters) => {
|
|
614
|
+
return clientRef.current.admin.listDomainRoles(filters);
|
|
615
|
+
};
|
|
616
|
+
return { users, agents, organizations, roles, loading, error, refresh, createUser, listDomainRoles };
|
|
617
|
+
}
|
|
618
|
+
function LoginButton({
|
|
619
|
+
config,
|
|
620
|
+
storageKey = "thalamus_auth",
|
|
621
|
+
label = "Login with ZEA",
|
|
622
|
+
scopes,
|
|
623
|
+
className,
|
|
624
|
+
style
|
|
625
|
+
}) {
|
|
626
|
+
const { login, isAuthenticated, isLoading, error } = useThalamus({ ...config, storageKey });
|
|
627
|
+
if (isLoading) return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#8b949e", fontSize: 14 }, children: "Loading..." });
|
|
628
|
+
if (isAuthenticated) return null;
|
|
629
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }, children: [
|
|
630
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
631
|
+
"button",
|
|
632
|
+
{
|
|
633
|
+
onClick: () => login({ scope: scopes }),
|
|
634
|
+
className,
|
|
635
|
+
style: {
|
|
636
|
+
padding: "12px 32px",
|
|
637
|
+
borderRadius: 8,
|
|
638
|
+
border: "none",
|
|
639
|
+
cursor: "pointer",
|
|
640
|
+
background: "#238636",
|
|
641
|
+
color: "#fff",
|
|
642
|
+
fontSize: 16,
|
|
643
|
+
fontWeight: 600,
|
|
644
|
+
fontFamily: "system-ui, sans-serif",
|
|
645
|
+
...style
|
|
646
|
+
},
|
|
647
|
+
children: label
|
|
648
|
+
}
|
|
649
|
+
),
|
|
650
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#ff7b72", fontSize: 12 }, children: error })
|
|
651
|
+
] });
|
|
652
|
+
}
|
|
653
|
+
function RegisterButton({ config, orgName, appOrigin, label = "Create Account", className, style }) {
|
|
654
|
+
const handleClick = () => {
|
|
655
|
+
const authParams = new URLSearchParams({
|
|
656
|
+
client_id: config.clientId,
|
|
657
|
+
redirect_uri: config.redirectUri,
|
|
658
|
+
response_type: "code",
|
|
659
|
+
scope: (config.defaultScopes || ["openid", "profile", "email"]).join(" "),
|
|
660
|
+
state: crypto.randomUUID()
|
|
661
|
+
});
|
|
662
|
+
if (orgName) authParams.set("org_name", orgName);
|
|
663
|
+
if (appOrigin) authParams.set("app_origin", appOrigin);
|
|
664
|
+
const returnTo = `/oauth/authorize?${authParams.toString()}`;
|
|
665
|
+
window.location.href = `${config.baseUrl}/register?return_to=${encodeURIComponent(returnTo)}`;
|
|
666
|
+
};
|
|
667
|
+
return /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleClick, className, style: { padding: "12px 32px", borderRadius: 8, border: "1px solid #30363d", cursor: "pointer", background: "transparent", color: "#e6edf3", fontSize: 16, fontWeight: 600, fontFamily: "system-ui, sans-serif", ...style }, children: label });
|
|
668
|
+
}
|
|
669
|
+
function UserMenu({ config, storageKey = "thalamus_auth", renderUser, className }) {
|
|
670
|
+
const { user, logout, isAuthenticated, isLoading, token } = useThalamus({ ...config, storageKey });
|
|
671
|
+
if (isLoading || !isAuthenticated || !user) return null;
|
|
672
|
+
const initials = user.name ? user.name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2) : (user.email || user.sub).slice(0, 2).toUpperCase();
|
|
673
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { display: "flex", alignItems: "center", gap: 10, fontFamily: "system-ui, sans-serif" }, children: [
|
|
674
|
+
renderUser ? renderUser(user) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
675
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: {
|
|
676
|
+
width: 28,
|
|
677
|
+
height: 28,
|
|
678
|
+
borderRadius: "50%",
|
|
679
|
+
background: "#238636",
|
|
680
|
+
color: "#fff",
|
|
681
|
+
display: "flex",
|
|
682
|
+
alignItems: "center",
|
|
683
|
+
justifyContent: "center",
|
|
684
|
+
fontSize: 11,
|
|
685
|
+
fontWeight: 700,
|
|
686
|
+
flexShrink: 0
|
|
687
|
+
}, children: user.picture ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: user.picture, alt: "", style: { width: 28, height: 28, borderRadius: "50%" } }) : initials }),
|
|
688
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { minWidth: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 12, fontWeight: 600, color: "#e6edf3", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: user.name || user.email || user.sub }) })
|
|
689
|
+
] }),
|
|
690
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
691
|
+
"button",
|
|
692
|
+
{
|
|
693
|
+
onClick: logout,
|
|
694
|
+
style: {
|
|
695
|
+
background: "none",
|
|
696
|
+
border: "1px solid #30363d",
|
|
697
|
+
color: "#8b949e",
|
|
698
|
+
padding: "4px 10px",
|
|
699
|
+
borderRadius: 6,
|
|
700
|
+
cursor: "pointer",
|
|
701
|
+
fontSize: 11,
|
|
702
|
+
fontFamily: "inherit"
|
|
703
|
+
},
|
|
704
|
+
children: "Logout"
|
|
705
|
+
}
|
|
706
|
+
)
|
|
707
|
+
] });
|
|
708
|
+
}
|
|
709
|
+
function UserCreateForm({ config, onCreated, className }) {
|
|
710
|
+
const [email, setEmail] = react.useState("");
|
|
711
|
+
const [password, setPassword] = react.useState("");
|
|
712
|
+
const [confirmPassword, setConfirmPassword] = react.useState("");
|
|
713
|
+
const [name, setName] = react.useState("");
|
|
714
|
+
const [isAgent, setIsAgent] = react.useState(false);
|
|
715
|
+
const [loading, setLoading] = react.useState(false);
|
|
716
|
+
const [error, setError] = react.useState("");
|
|
717
|
+
const [showPassword, setShowPassword] = react.useState(false);
|
|
718
|
+
const generatePassword = () => {
|
|
719
|
+
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
|
720
|
+
let pass = "";
|
|
721
|
+
for (let i = 0; i < 16; i++) {
|
|
722
|
+
pass += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
723
|
+
}
|
|
724
|
+
setPassword(pass);
|
|
725
|
+
setConfirmPassword(pass);
|
|
726
|
+
setShowPassword(true);
|
|
727
|
+
};
|
|
728
|
+
const handleSubmit = async (e) => {
|
|
729
|
+
e.preventDefault();
|
|
730
|
+
if (password !== confirmPassword) {
|
|
731
|
+
setError("Passwords do not match");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
setLoading(true);
|
|
735
|
+
setError("");
|
|
736
|
+
try {
|
|
737
|
+
const client = new ThalamusClient(config);
|
|
738
|
+
const user = await client.admin.createUser({ email, password, name, is_agent: isAgent });
|
|
739
|
+
setEmail("");
|
|
740
|
+
setPassword("");
|
|
741
|
+
setConfirmPassword("");
|
|
742
|
+
setName("");
|
|
743
|
+
setIsAgent(false);
|
|
744
|
+
setShowPassword(false);
|
|
745
|
+
onCreated?.(user);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
setError(err.message);
|
|
748
|
+
}
|
|
749
|
+
setLoading(false);
|
|
750
|
+
};
|
|
751
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: `th-form ${className || ""}`, children: [
|
|
752
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "th-form-grid", children: [
|
|
753
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { required: true, placeholder: "Email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), className: "th-input" }),
|
|
754
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { placeholder: "Name (optional)", value: name, onChange: (e) => setName(e.target.value), className: "th-input" }),
|
|
755
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", display: "flex", gap: "8px" }, children: [
|
|
756
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { required: true, placeholder: "Password", type: showPassword ? "text" : "password", value: password, onChange: (e) => setPassword(e.target.value), className: "th-input", minLength: 8, style: { flex: 1, minWidth: 0 } }),
|
|
757
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: generatePassword, className: "th-btn th-btn--ghost", title: "Generate Password", style: { padding: "0 12px" }, children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" }) }) })
|
|
758
|
+
] }),
|
|
759
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { required: true, placeholder: "Confirm Password", type: showPassword ? "text" : "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), className: "th-input", minLength: 8 }),
|
|
760
|
+
/* @__PURE__ */ jsxRuntime.jsxs("select", { value: isAgent ? "agent" : "user", onChange: (e) => setIsAgent(e.target.value === "agent"), className: "th-select", style: { gridColumn: "1 / -1" }, children: [
|
|
761
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "user", children: "Human User" }),
|
|
762
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "agent", children: "AI Agent" })
|
|
763
|
+
] })
|
|
764
|
+
] }),
|
|
765
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "th-alert", children: error }),
|
|
766
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: "8px" }, children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: loading, className: "th-btn th-btn--primary", children: loading ? "Creating..." : "Create User" }) })
|
|
767
|
+
] });
|
|
768
|
+
}
|
|
769
|
+
function UserTable({ users, loading, error, className }) {
|
|
770
|
+
if (loading) return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "th-loading", children: "Loading..." });
|
|
771
|
+
if (error) return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "th-alert", children: error });
|
|
772
|
+
if (users.length === 0) return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "th-empty", children: "No users found." });
|
|
773
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `th-table-container ${className || ""}`, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "th-table", children: [
|
|
774
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
775
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "Name" }),
|
|
776
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "Email" }),
|
|
777
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "Organization" }),
|
|
778
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "User Type" }),
|
|
779
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "Status" })
|
|
780
|
+
] }) }),
|
|
781
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: users.map((u) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
782
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { children: u.name || "\u2014" }),
|
|
783
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "th-text-accent", children: u.email }),
|
|
784
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { style: { color: "var(--th-text-muted)" }, children: u.organization_id ? u.organization_id.split("-")[0] + "..." : "\u2014" }),
|
|
785
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { style: { display: "flex", alignItems: "center", gap: 6, padding: "10px 16px" }, children: u.is_agent ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
786
|
+
/* @__PURE__ */ jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
787
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { width: "18", height: "14", x: "3", y: "7", rx: "2", ry: "2" }),
|
|
788
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 7V3" }),
|
|
789
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 3h6" }),
|
|
790
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 11h.01" }),
|
|
791
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 15h.01" }),
|
|
792
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 15h.01" })
|
|
793
|
+
] }),
|
|
794
|
+
" Agent"
|
|
795
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
796
|
+
/* @__PURE__ */ jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
797
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }),
|
|
798
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "7", r: "4" })
|
|
799
|
+
] }),
|
|
800
|
+
" User"
|
|
801
|
+
] }) }),
|
|
802
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { children: /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: u.status }) })
|
|
803
|
+
] }, u.id)) })
|
|
804
|
+
] }) });
|
|
805
|
+
}
|
|
806
|
+
function StatusBadge({ status }) {
|
|
807
|
+
const badgeClass = status ? `th-badge--${status}` : "";
|
|
808
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `th-badge ${badgeClass}`, children: status || "unknown" });
|
|
809
|
+
}
|
|
810
|
+
function APIKeyManager({ baseUrl, authStorageKey = "thalamus_auth", label = "+ Generate API Key", className }) {
|
|
811
|
+
const [keys, setKeys] = react.useState([]);
|
|
812
|
+
const [newKey, setNewKey] = react.useState(null);
|
|
813
|
+
const [loading, setLoading] = react.useState(false);
|
|
814
|
+
const [error, setError] = react.useState("");
|
|
815
|
+
const token = typeof window !== "undefined" ? (() => {
|
|
816
|
+
try {
|
|
817
|
+
const s = localStorage.getItem(authStorageKey);
|
|
818
|
+
return s ? JSON.parse(s).accessToken : null;
|
|
819
|
+
} catch {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
})() : null;
|
|
823
|
+
react.useEffect(() => {
|
|
824
|
+
if (!token) return;
|
|
825
|
+
const controller = new AbortController();
|
|
826
|
+
fetch(`${baseUrl}/api/personal-access-tokens`, {
|
|
827
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
828
|
+
signal: controller.signal
|
|
829
|
+
}).then((res) => res.json()).then((data) => {
|
|
830
|
+
if (data.data) {
|
|
831
|
+
setKeys(data.data.map((k) => ({
|
|
832
|
+
api_key: k.api_key || k.id,
|
|
833
|
+
prefix: k.token_prefix || k.prefix || "zpat_",
|
|
834
|
+
created: k.inserted_at || k.created_at || k.created || (/* @__PURE__ */ new Date()).toISOString()
|
|
835
|
+
})));
|
|
836
|
+
}
|
|
837
|
+
}).catch((e) => {
|
|
838
|
+
if (e.name !== "AbortError") console.error("Failed to load API keys:", e);
|
|
839
|
+
});
|
|
840
|
+
return () => controller.abort();
|
|
841
|
+
}, [baseUrl, token]);
|
|
842
|
+
const createKey = async () => {
|
|
843
|
+
setLoading(true);
|
|
844
|
+
setError("");
|
|
845
|
+
try {
|
|
846
|
+
const res = await fetch(`${baseUrl}/api/personal-access-tokens`, {
|
|
847
|
+
method: "POST",
|
|
848
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
849
|
+
body: JSON.stringify({ name: `key-${Date.now()}`, scopes: ["api:read", "api:write"] })
|
|
850
|
+
});
|
|
851
|
+
const data = await res.json();
|
|
852
|
+
if (data.api_key) {
|
|
853
|
+
setNewKey(data.api_key);
|
|
854
|
+
setKeys((prev) => [...prev, { api_key: data.api_key, prefix: data.prefix, created: (/* @__PURE__ */ new Date()).toISOString() }]);
|
|
855
|
+
} else setError(data.error || "Failed to create key");
|
|
856
|
+
} catch (e) {
|
|
857
|
+
setError(e.message);
|
|
858
|
+
}
|
|
859
|
+
setLoading(false);
|
|
860
|
+
};
|
|
861
|
+
const copyKey = (key) => {
|
|
862
|
+
navigator.clipboard?.writeText(key);
|
|
863
|
+
};
|
|
864
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { fontFamily: "system-ui, sans-serif" }, children: [
|
|
865
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "8px 12px", background: "#fff0f0", borderRadius: 6, color: "#c00", marginBottom: 12, fontSize: 12 }, children: error }),
|
|
866
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: createKey, disabled: loading, style: {
|
|
867
|
+
padding: "8px 20px",
|
|
868
|
+
borderRadius: 6,
|
|
869
|
+
border: "none",
|
|
870
|
+
cursor: "pointer",
|
|
871
|
+
background: "#238636",
|
|
872
|
+
color: "#fff",
|
|
873
|
+
fontSize: 13,
|
|
874
|
+
fontWeight: 600,
|
|
875
|
+
opacity: loading ? 0.7 : 1,
|
|
876
|
+
fontFamily: "inherit"
|
|
877
|
+
}, children: loading ? "Generating..." : label }),
|
|
878
|
+
newKey && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 16, padding: "12px 16px", background: "#f0fff0", borderRadius: 8, border: "1px solid #2ea44f" }, children: [
|
|
879
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 12, fontWeight: 600, color: "#2ea44f", marginBottom: 6 }, children: "\u2705 Key created \u2014 copy it now" }),
|
|
880
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
|
|
881
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { style: { flex: 1, padding: "8px 12px", background: "#fff", borderRadius: 4, fontSize: 11, fontFamily: "monospace", wordBreak: "break-all", userSelect: "all" }, children: newKey }),
|
|
882
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => copyKey(newKey), style: { padding: "6px 12px", borderRadius: 4, border: "1px solid #2ea44f", background: "#dafbe1", color: "#1a7f37", cursor: "pointer", fontSize: 11, fontFamily: "inherit" }, children: "Copy" })
|
|
883
|
+
] })
|
|
884
|
+
] }),
|
|
885
|
+
keys.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 20 }, children: [
|
|
886
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 12, fontWeight: 600, marginBottom: 8 }, children: [
|
|
887
|
+
"Existing keys (",
|
|
888
|
+
keys.length,
|
|
889
|
+
")"
|
|
890
|
+
] }),
|
|
891
|
+
keys.map((k, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "6px 0", borderBottom: "1px solid #eee", fontSize: 11, fontFamily: "monospace", color: "#656d76" }, children: [
|
|
892
|
+
k.prefix,
|
|
893
|
+
"...",
|
|
894
|
+
k.api_key?.slice(-8),
|
|
895
|
+
" \xB7 ",
|
|
896
|
+
new Date(k.created).toLocaleDateString()
|
|
897
|
+
] }, i))
|
|
898
|
+
] })
|
|
899
|
+
] });
|
|
900
|
+
}
|
|
901
|
+
function OrgSwitcher({ config, onSwitch, className, style }) {
|
|
902
|
+
const [orgs, setOrgs] = react.useState([]);
|
|
903
|
+
const [selected, setSelected] = react.useState(() => {
|
|
904
|
+
try {
|
|
905
|
+
const authStr = typeof window !== "undefined" ? localStorage.getItem("thalamus_auth") : null;
|
|
906
|
+
if (authStr) {
|
|
907
|
+
const auth = JSON.parse(authStr);
|
|
908
|
+
return auth.user?.organization_id || "";
|
|
909
|
+
}
|
|
910
|
+
} catch (e) {
|
|
911
|
+
console.error(e);
|
|
912
|
+
}
|
|
913
|
+
return "";
|
|
914
|
+
});
|
|
915
|
+
const [loading, setLoading] = react.useState(true);
|
|
916
|
+
react.useEffect(() => {
|
|
917
|
+
const client = new ThalamusClient(config);
|
|
918
|
+
client.admin.listOrganizations().then((o) => {
|
|
919
|
+
setOrgs(o);
|
|
920
|
+
setLoading(false);
|
|
921
|
+
if (o.length > 0) {
|
|
922
|
+
let currentSelected = "";
|
|
923
|
+
try {
|
|
924
|
+
const authStr = typeof window !== "undefined" ? localStorage.getItem("thalamus_auth") : null;
|
|
925
|
+
if (authStr) {
|
|
926
|
+
const auth = JSON.parse(authStr);
|
|
927
|
+
currentSelected = auth.user?.organization_id || "";
|
|
928
|
+
}
|
|
929
|
+
} catch (e) {
|
|
930
|
+
}
|
|
931
|
+
if (!currentSelected) {
|
|
932
|
+
const defaultOrgId = "ea7b11ea-852c-44e5-aee1-a761ec76eaea";
|
|
933
|
+
const target = o.find((org) => org.id === defaultOrgId) ? defaultOrgId : o[0].id;
|
|
934
|
+
setSelected(target);
|
|
935
|
+
onSwitch?.(target);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}).catch(() => setLoading(false));
|
|
939
|
+
}, [config.baseUrl]);
|
|
940
|
+
if (loading) return null;
|
|
941
|
+
if (orgs.length === 0) return null;
|
|
942
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
943
|
+
"select",
|
|
944
|
+
{
|
|
945
|
+
className: `th-select ${className || ""}`,
|
|
946
|
+
value: selected,
|
|
947
|
+
onChange: (e) => {
|
|
948
|
+
setSelected(e.target.value);
|
|
949
|
+
onSwitch?.(e.target.value);
|
|
950
|
+
},
|
|
951
|
+
style,
|
|
952
|
+
children: [
|
|
953
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select org..." }),
|
|
954
|
+
orgs.map((org) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: org.id, children: org.name }, org.id))
|
|
955
|
+
]
|
|
956
|
+
}
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
function OrgManager({ config, className }) {
|
|
960
|
+
const [orgs, setOrgs] = react.useState([]);
|
|
961
|
+
const [loading, setLoading] = react.useState(true);
|
|
962
|
+
const [error, setError] = react.useState("");
|
|
963
|
+
react.useEffect(() => {
|
|
964
|
+
const client = new ThalamusClient(config);
|
|
965
|
+
client.admin.listOrganizations().then((o) => {
|
|
966
|
+
setOrgs(o);
|
|
967
|
+
setLoading(false);
|
|
968
|
+
}).catch((e) => {
|
|
969
|
+
setError(e.message);
|
|
970
|
+
setLoading(false);
|
|
971
|
+
});
|
|
972
|
+
}, [config.baseUrl, config.clientId, config.redirectUri]);
|
|
973
|
+
if (loading) return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "th-loading", children: "Loading..." });
|
|
974
|
+
if (error) return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "th-alert", children: error });
|
|
975
|
+
if (orgs.length === 0) return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "th-empty", children: "No organizations." });
|
|
976
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `th-list ${className || ""}`, children: orgs.map((org) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "th-card", children: [
|
|
977
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "th-card-title", children: org.name }),
|
|
978
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "th-card-desc", children: [
|
|
979
|
+
"Domains: ",
|
|
980
|
+
(org.domains || []).join(", ") || "none"
|
|
981
|
+
] })
|
|
982
|
+
] }, org.id)) });
|
|
983
|
+
}
|
|
984
|
+
var Z = {
|
|
985
|
+
mu: "#8b949e",
|
|
986
|
+
pr: "#58a6ff"};
|
|
987
|
+
function ThalamusPanel({ config, onNavigate }) {
|
|
988
|
+
const [view, setView] = react.useState("users");
|
|
989
|
+
const handleNav = (v) => {
|
|
990
|
+
setView(v);
|
|
991
|
+
onNavigate?.(v);
|
|
992
|
+
};
|
|
993
|
+
const navItem = (v, label, icon) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
994
|
+
"button",
|
|
995
|
+
{
|
|
996
|
+
onClick: () => handleNav(v),
|
|
997
|
+
style: {
|
|
998
|
+
display: "flex",
|
|
999
|
+
alignItems: "center",
|
|
1000
|
+
gap: 8,
|
|
1001
|
+
width: "100%",
|
|
1002
|
+
padding: "6px 16px",
|
|
1003
|
+
border: "none",
|
|
1004
|
+
cursor: "pointer",
|
|
1005
|
+
background: view === v ? `${Z.pr}15` : "transparent",
|
|
1006
|
+
color: view === v ? Z.pr : Z.mu,
|
|
1007
|
+
fontSize: 13,
|
|
1008
|
+
fontFamily: "system-ui, sans-serif",
|
|
1009
|
+
textAlign: "left"
|
|
1010
|
+
},
|
|
1011
|
+
children: [
|
|
1012
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 14, width: 20, textAlign: "center" }, children: icon }),
|
|
1013
|
+
label
|
|
1014
|
+
]
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1018
|
+
navItem("users", "Users & Agents", "\u{1F464}"),
|
|
1019
|
+
navItem("orgs", "Organizations", "\u{1F3E2}"),
|
|
1020
|
+
navItem("apikeys", "API Keys", "\u{1F511}")
|
|
1021
|
+
] });
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
exports.APIKeyManager = APIKeyManager;
|
|
1025
|
+
exports.AdminAPI = AdminAPI;
|
|
1026
|
+
exports.LoginButton = LoginButton;
|
|
1027
|
+
exports.OAuth2 = OAuth2;
|
|
1028
|
+
exports.OrgManager = OrgManager;
|
|
1029
|
+
exports.OrgSwitcher = OrgSwitcher;
|
|
1030
|
+
exports.RegisterButton = RegisterButton;
|
|
1031
|
+
exports.StatusBadge = StatusBadge;
|
|
1032
|
+
exports.ThalamusClient = ThalamusClient;
|
|
1033
|
+
exports.ThalamusPanel = ThalamusPanel;
|
|
1034
|
+
exports.TokenManager = TokenManager;
|
|
1035
|
+
exports.UserCreateForm = UserCreateForm;
|
|
1036
|
+
exports.UserMenu = UserMenu;
|
|
1037
|
+
exports.UserTable = UserTable;
|
|
1038
|
+
exports.useAdmin = useAdmin;
|
|
1039
|
+
exports.useThalamus = useThalamus;
|
|
1040
|
+
//# sourceMappingURL=index.js.map
|
|
1041
|
+
//# sourceMappingURL=index.js.map
|