@luxfi/ui 7.4.6 → 7.4.8
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/dist/alert.cjs +3 -7
- package/dist/alert.js +3 -7
- package/dist/auth.cjs +481 -0
- package/dist/auth.d.cts +80 -0
- package/dist/auth.d.ts +80 -0
- package/dist/auth.js +456 -0
- package/dist/badge.cjs +4 -8
- package/dist/badge.js +4 -8
- package/dist/button.cjs +3 -7
- package/dist/button.js +3 -7
- package/dist/collapsible.cjs +3 -7
- package/dist/collapsible.js +3 -7
- package/dist/icon-button.cjs +3 -7
- package/dist/icon-button.js +3 -7
- package/dist/icons.cjs +0 -1
- package/dist/icons.js +0 -1
- package/dist/image.cjs +3 -7
- package/dist/image.js +3 -7
- package/dist/index.cjs +9 -8
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -9
- package/dist/link.cjs +3 -7
- package/dist/link.js +3 -7
- package/dist/next.cjs +20 -1
- package/dist/next.js +20 -1
- package/dist/select.cjs +3 -7
- package/dist/select.js +3 -7
- package/dist/skeleton.cjs +8 -7
- package/dist/skeleton.d.cts +17 -1
- package/dist/skeleton.d.ts +17 -1
- package/dist/skeleton.js +8 -8
- package/dist/table.cjs +3 -7
- package/dist/table.js +3 -7
- package/dist/tag.cjs +4 -8
- package/dist/tag.js +4 -8
- package/dist/tooltip.cjs +1 -1
- package/dist/tooltip.js +1 -1
- package/dist/vite.cjs +4 -3
- package/dist/vite.js +4 -3
- package/package.json +12 -2
- package/src/auth.tsx +198 -0
- package/src/engine.ts +40 -3
- package/src/index.ts +1 -1
- package/src/next.ts +23 -0
- package/src/skeleton.tsx +36 -7
- package/src/tooltip.tsx +13 -5
- package/src/vite.ts +4 -9
- package/tokens.css +217 -0
package/dist/alert.cjs
CHANGED
|
@@ -207,11 +207,7 @@ function stripShims(props) {
|
|
|
207
207
|
for (const k of SHIM_PROPS) delete out[k];
|
|
208
208
|
return out;
|
|
209
209
|
}
|
|
210
|
-
var SKELETON_CLASSES =
|
|
211
|
-
"animate-skeleton-shimmer rounded-sm",
|
|
212
|
-
"bg-[linear-gradient(90deg,var(--color-skeleton-start)_0%,var(--color-skeleton-end)_50%,var(--color-skeleton-start)_100%)]",
|
|
213
|
-
"bg-[length:200%_100%]"
|
|
214
|
-
].join(" ");
|
|
210
|
+
var SKELETON_CLASSES = "lux-skeleton rounded-sm";
|
|
215
211
|
var HIDE_BELOW_MAP = { lg: "lg:hidden", md: "md:hidden", sm: "sm:hidden" };
|
|
216
212
|
var Skeleton = React2__namespace.forwardRef(
|
|
217
213
|
function Skeleton2(props, ref) {
|
|
@@ -231,7 +227,7 @@ var Skeleton = React2__namespace.forwardRef(
|
|
|
231
227
|
{
|
|
232
228
|
ref,
|
|
233
229
|
"data-loading": true,
|
|
234
|
-
className: cn(SKELETON_CLASSES, "
|
|
230
|
+
className: cn(SKELETON_CLASSES, "[&_*]:invisible", cls),
|
|
235
231
|
style: mergedStyle,
|
|
236
232
|
...htmlRest,
|
|
237
233
|
children
|
|
@@ -243,7 +239,7 @@ var Skeleton = React2__namespace.forwardRef(
|
|
|
243
239
|
{
|
|
244
240
|
ref,
|
|
245
241
|
"data-loading": true,
|
|
246
|
-
className: cn(SKELETON_CLASSES, children ? "
|
|
242
|
+
className: cn(SKELETON_CLASSES, children ? "[&_*]:invisible" : "min-h-5", cls),
|
|
247
243
|
style: mergedStyle,
|
|
248
244
|
...htmlRest,
|
|
249
245
|
children
|
package/dist/alert.js
CHANGED
|
@@ -185,11 +185,7 @@ function stripShims(props) {
|
|
|
185
185
|
for (const k of SHIM_PROPS) delete out[k];
|
|
186
186
|
return out;
|
|
187
187
|
}
|
|
188
|
-
var SKELETON_CLASSES =
|
|
189
|
-
"animate-skeleton-shimmer rounded-sm",
|
|
190
|
-
"bg-[linear-gradient(90deg,var(--color-skeleton-start)_0%,var(--color-skeleton-end)_50%,var(--color-skeleton-start)_100%)]",
|
|
191
|
-
"bg-[length:200%_100%]"
|
|
192
|
-
].join(" ");
|
|
188
|
+
var SKELETON_CLASSES = "lux-skeleton rounded-sm";
|
|
193
189
|
var HIDE_BELOW_MAP = { lg: "lg:hidden", md: "md:hidden", sm: "sm:hidden" };
|
|
194
190
|
var Skeleton = React2.forwardRef(
|
|
195
191
|
function Skeleton2(props, ref) {
|
|
@@ -209,7 +205,7 @@ var Skeleton = React2.forwardRef(
|
|
|
209
205
|
{
|
|
210
206
|
ref,
|
|
211
207
|
"data-loading": true,
|
|
212
|
-
className: cn(SKELETON_CLASSES, "
|
|
208
|
+
className: cn(SKELETON_CLASSES, "[&_*]:invisible", cls),
|
|
213
209
|
style: mergedStyle,
|
|
214
210
|
...htmlRest,
|
|
215
211
|
children
|
|
@@ -221,7 +217,7 @@ var Skeleton = React2.forwardRef(
|
|
|
221
217
|
{
|
|
222
218
|
ref,
|
|
223
219
|
"data-loading": true,
|
|
224
|
-
className: cn(SKELETON_CLASSES, children ? "
|
|
220
|
+
className: cn(SKELETON_CLASSES, children ? "[&_*]:invisible" : "min-h-5", cls),
|
|
225
221
|
style: mergedStyle,
|
|
226
222
|
...htmlRest,
|
|
227
223
|
children
|
package/dist/auth.cjs
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var browser = require('@hanzo/iam/browser');
|
|
5
|
+
var React2 = require('react');
|
|
6
|
+
require('@hanzo/gui');
|
|
7
|
+
var reactQuery = require('@tanstack/react-query');
|
|
8
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
|
+
|
|
10
|
+
function _interopNamespace(e) {
|
|
11
|
+
if (e && e.__esModule) return e;
|
|
12
|
+
var n = Object.create(null);
|
|
13
|
+
if (e) {
|
|
14
|
+
Object.keys(e).forEach(function (k) {
|
|
15
|
+
if (k !== 'default') {
|
|
16
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function () { return e[k]; }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
|
|
29
|
+
|
|
30
|
+
// src/white-label.ts
|
|
31
|
+
var LUX_BRAND = {
|
|
32
|
+
lux: "#7000FF",
|
|
33
|
+
zoo: "#6C5EFB",
|
|
34
|
+
hanzo: "#EA580C",
|
|
35
|
+
pars: "#1C3879"};
|
|
36
|
+
var PARS_GOLD = "#C8A45C";
|
|
37
|
+
var ORGS = {
|
|
38
|
+
lux: {
|
|
39
|
+
org: "lux",
|
|
40
|
+
name: "Lux",
|
|
41
|
+
domain: "lux.network",
|
|
42
|
+
iamDomain: "lux.id",
|
|
43
|
+
accent: LUX_BRAND.lux,
|
|
44
|
+
accentForeground: "#FFFFFF"
|
|
45
|
+
},
|
|
46
|
+
zoo: {
|
|
47
|
+
org: "zoo",
|
|
48
|
+
name: "Zoo",
|
|
49
|
+
domain: "zoo.ngo",
|
|
50
|
+
// Zoo's issuer is `zoolabs.id` — the one row where it is not `<org>.id`,
|
|
51
|
+
// and the one every hand-rolled branding map gets wrong. `id.zoo.network`
|
|
52
|
+
// ANSWERS, which is why it keeps being copied around, but it is an alias:
|
|
53
|
+
// its own discovery document advertises
|
|
54
|
+
// issuer https://zoolabs.id
|
|
55
|
+
// authorization_endpoint https://zoolabs.id/v1/iam/oauth/authorize
|
|
56
|
+
// jwks_uri https://zoolabs.id/v1/iam/.well-known/jwks
|
|
57
|
+
// so a client configured on the alias validates `iss` against a string the
|
|
58
|
+
// IdP never emits and rejects every token it is given — the exact shape of
|
|
59
|
+
// the lux.id outage. `zoo.id` is not an IdP at all (no discovery document).
|
|
60
|
+
iamDomain: "zoolabs.id",
|
|
61
|
+
accent: LUX_BRAND.zoo,
|
|
62
|
+
accentForeground: "#FFFFFF"
|
|
63
|
+
},
|
|
64
|
+
bootnode: {
|
|
65
|
+
org: "bootnode",
|
|
66
|
+
name: "Bootnode",
|
|
67
|
+
domain: "bootno.de",
|
|
68
|
+
iamDomain: "id.bootno.de",
|
|
69
|
+
accent: LUX_BRAND.lux,
|
|
70
|
+
accentForeground: "#FFFFFF"
|
|
71
|
+
},
|
|
72
|
+
hanzo: {
|
|
73
|
+
org: "hanzo",
|
|
74
|
+
name: "Hanzo",
|
|
75
|
+
domain: "hanzo.ai",
|
|
76
|
+
iamDomain: "hanzo.id",
|
|
77
|
+
accent: LUX_BRAND.hanzo,
|
|
78
|
+
accentForeground: "#FFFFFF"
|
|
79
|
+
},
|
|
80
|
+
pars: {
|
|
81
|
+
org: "pars",
|
|
82
|
+
name: "Pars",
|
|
83
|
+
domain: "pars.id",
|
|
84
|
+
iamDomain: "pars.id",
|
|
85
|
+
accent: LUX_BRAND.pars,
|
|
86
|
+
accentForeground: PARS_GOLD
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var DOMAIN_ORG = [
|
|
90
|
+
["lux.network", "lux"],
|
|
91
|
+
["lux.cloud", "lux"],
|
|
92
|
+
["lux.id", "lux"],
|
|
93
|
+
["lux.finance", "lux"],
|
|
94
|
+
["lux.market", "lux"],
|
|
95
|
+
["lux.exchange", "lux"],
|
|
96
|
+
["zoo.ngo", "zoo"],
|
|
97
|
+
["zoo.network", "zoo"],
|
|
98
|
+
["zoo.cloud", "zoo"],
|
|
99
|
+
["zoolabs.id", "zoo"],
|
|
100
|
+
["hanzo.ai", "hanzo"],
|
|
101
|
+
["hanzo.cloud", "hanzo"],
|
|
102
|
+
["hanzo.id", "hanzo"],
|
|
103
|
+
["pars.id", "pars", "app"],
|
|
104
|
+
["pars.network", "pars", "app"],
|
|
105
|
+
["parsdao.org", "pars", "app"],
|
|
106
|
+
["bootno.de", "bootnode", "platform"]
|
|
107
|
+
];
|
|
108
|
+
var DEFAULT_ORG = "lux";
|
|
109
|
+
function normalizeHost(host) {
|
|
110
|
+
const noPort = host.trim().toLowerCase().split(":")[0] ?? "";
|
|
111
|
+
return noPort.replace(/\.$/, "");
|
|
112
|
+
}
|
|
113
|
+
function isUnder(host, domain) {
|
|
114
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
115
|
+
}
|
|
116
|
+
function matchDomain(host) {
|
|
117
|
+
let best;
|
|
118
|
+
for (const entry of DOMAIN_ORG) {
|
|
119
|
+
if (isUnder(host, entry[0]) && (!best || entry[0].length > best[0].length)) {
|
|
120
|
+
best = entry;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return best;
|
|
124
|
+
}
|
|
125
|
+
function appSlug(host, domain, pinned) {
|
|
126
|
+
if (pinned) return pinned;
|
|
127
|
+
const sub = host === domain ? "" : host.slice(0, -(domain.length + 1));
|
|
128
|
+
const leftmost = sub.split(".")[0] ?? "";
|
|
129
|
+
if (leftmost && leftmost !== "www") return leftmost;
|
|
130
|
+
const label = domain.split(".")[0] ?? "";
|
|
131
|
+
const tld = domain.split(".").slice(1).join(".");
|
|
132
|
+
return tld.split(".")[0] || label;
|
|
133
|
+
}
|
|
134
|
+
function isLocal(host) {
|
|
135
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "0.0.0.0" || host === "[::1]" || host.endsWith(".local");
|
|
136
|
+
}
|
|
137
|
+
function resolveWhiteLabel(host) {
|
|
138
|
+
const h = normalizeHost(host ?? "");
|
|
139
|
+
const match = h ? matchDomain(h) : void 0;
|
|
140
|
+
const org = match ? match[1] : DEFAULT_ORG;
|
|
141
|
+
const identity = ORGS[org];
|
|
142
|
+
const app = match ? appSlug(h, match[0], match[2]) : h && isLocal(h) ? "local" : "app";
|
|
143
|
+
return {
|
|
144
|
+
...identity,
|
|
145
|
+
host: h,
|
|
146
|
+
app,
|
|
147
|
+
theme: `dark_${org}`,
|
|
148
|
+
issuer: `https://${identity.iamDomain}`,
|
|
149
|
+
clientId: `${org}-${app}`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/iam.ts
|
|
154
|
+
var IamError = class extends Error {
|
|
155
|
+
constructor(message, status, unauthenticated) {
|
|
156
|
+
super(message);
|
|
157
|
+
this.name = "IamError";
|
|
158
|
+
this.status = status;
|
|
159
|
+
this.unauthenticated = unauthenticated;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var SIGNED_OUT = /please sign in|authentication required|unauthorized/i;
|
|
163
|
+
function isSignedOut(status, msg) {
|
|
164
|
+
return status === 401 || SIGNED_OUT.test(msg);
|
|
165
|
+
}
|
|
166
|
+
function iamBase(host) {
|
|
167
|
+
const wl = typeof host === "object" && host !== null ? host : resolveWhiteLabel(host);
|
|
168
|
+
return `${wl.issuer}/v1/iam`;
|
|
169
|
+
}
|
|
170
|
+
async function read(call, verb, params) {
|
|
171
|
+
const url = new URL(`${call.base}/${verb}`);
|
|
172
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
|
|
173
|
+
let res;
|
|
174
|
+
try {
|
|
175
|
+
res = await fetch(url.toString(), {
|
|
176
|
+
method: "GET",
|
|
177
|
+
// The bearer is the whole credential. IAM does not allow credentialed
|
|
178
|
+
// CORS, so sending cookies would only get the response blocked.
|
|
179
|
+
headers: call.token ? { Authorization: `Bearer ${call.token}` } : {},
|
|
180
|
+
signal: call.signal
|
|
181
|
+
});
|
|
182
|
+
} catch (e) {
|
|
183
|
+
throw new IamError(e instanceof Error ? e.message : "IAM unreachable", 0, false);
|
|
184
|
+
}
|
|
185
|
+
const body = await res.json().catch(() => ({}));
|
|
186
|
+
const msg = typeof body.msg === "string" ? body.msg : "";
|
|
187
|
+
if (!res.ok || body.status === "error") {
|
|
188
|
+
throw new IamError(msg || `IAM ${verb} failed (${res.status})`, res.status, isSignedOut(res.status, msg));
|
|
189
|
+
}
|
|
190
|
+
return body.data;
|
|
191
|
+
}
|
|
192
|
+
var IAM_PAGE_SIZE = 20;
|
|
193
|
+
var iam = {
|
|
194
|
+
/** The signed-in principal, or null when there is no session. */
|
|
195
|
+
async account(call) {
|
|
196
|
+
try {
|
|
197
|
+
const data = await read(call, "get-account", {});
|
|
198
|
+
return data && data.name ? data : null;
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (e instanceof IamError && e.unauthenticated) return null;
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
/**
|
|
205
|
+
* One page of the orgs the principal can see. IAM scopes the read to the
|
|
206
|
+
* caller's authority, so a normal member gets their own org and a super
|
|
207
|
+
* admin gets the tenant list — the page never has to decide.
|
|
208
|
+
*/
|
|
209
|
+
async organizations(call, page = 0, query = "", pageSize = IAM_PAGE_SIZE) {
|
|
210
|
+
const params = {
|
|
211
|
+
owner: "admin",
|
|
212
|
+
// IAM orgs live under the reserved `admin` owner
|
|
213
|
+
p: page + 1,
|
|
214
|
+
// the backend page is 1-based
|
|
215
|
+
pageSize,
|
|
216
|
+
sortField: "name",
|
|
217
|
+
sortOrder: "ascending"
|
|
218
|
+
};
|
|
219
|
+
const q = query.trim();
|
|
220
|
+
if (q) {
|
|
221
|
+
params.field = "name";
|
|
222
|
+
params.value = q;
|
|
223
|
+
}
|
|
224
|
+
return await read(call, "get-organizations", params) ?? [];
|
|
225
|
+
},
|
|
226
|
+
/** One organization by name. */
|
|
227
|
+
async organization(call, name) {
|
|
228
|
+
return await read(call, "get-organization", { id: `admin/${name}` }) ?? null;
|
|
229
|
+
},
|
|
230
|
+
/** Members of an organization. */
|
|
231
|
+
async users(call, org) {
|
|
232
|
+
return await read(call, "get-users", { owner: org }) ?? [];
|
|
233
|
+
},
|
|
234
|
+
/** The organization's projects — IAM keys this read by `organization`. */
|
|
235
|
+
async projects(call, org) {
|
|
236
|
+
return await read(call, "get-organization-projects", { organization: org }) ?? [];
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
new reactQuery.QueryClient({
|
|
240
|
+
defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1, staleTime: 3e4 } }
|
|
241
|
+
});
|
|
242
|
+
var WhiteLabelContext = React2__namespace.default.createContext(resolveWhiteLabel(""));
|
|
243
|
+
var useWhiteLabel = () => React2__namespace.default.useContext(WhiteLabelContext);
|
|
244
|
+
var IAM_TOKEN_KEY = "hanzo_iam_access_token";
|
|
245
|
+
function storedToken() {
|
|
246
|
+
if (typeof window === "undefined") return null;
|
|
247
|
+
for (const store of [window.sessionStorage, window.localStorage]) {
|
|
248
|
+
try {
|
|
249
|
+
const t = store.getItem(IAM_TOKEN_KEY);
|
|
250
|
+
if (t) return t;
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function clearStoredTokens() {
|
|
257
|
+
if (typeof window === "undefined") return;
|
|
258
|
+
for (const store of [window.sessionStorage, window.localStorage]) {
|
|
259
|
+
try {
|
|
260
|
+
for (const k of ["access_token", "refresh_token", "id_token", "expires_at"]) {
|
|
261
|
+
store.removeItem(`hanzo_iam_${k}`);
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
var defaultAuth = {};
|
|
268
|
+
var scopeKey = (host, kind) => `lux-ui:${kind}:${host}`;
|
|
269
|
+
function readScope(host, kind) {
|
|
270
|
+
if (typeof window === "undefined") return null;
|
|
271
|
+
try {
|
|
272
|
+
return window.localStorage.getItem(scopeKey(host, kind));
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function writeScope(host, kind, value) {
|
|
278
|
+
if (typeof window === "undefined") return;
|
|
279
|
+
try {
|
|
280
|
+
window.localStorage.setItem(scopeKey(host, kind), value);
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
var IdentityContext = React2__namespace.createContext(null);
|
|
285
|
+
var IdentityProvider = ({
|
|
286
|
+
children,
|
|
287
|
+
auth = defaultAuth,
|
|
288
|
+
loginPath = "/login"
|
|
289
|
+
}) => {
|
|
290
|
+
const whiteLabel = useWhiteLabel();
|
|
291
|
+
const base = React2__namespace.useMemo(() => iamBase(whiteLabel), [whiteLabel]);
|
|
292
|
+
const [status, setStatus] = React2__namespace.useState("loading");
|
|
293
|
+
const [account, setAccount] = React2__namespace.useState(null);
|
|
294
|
+
const [orgs, setOrgs] = React2__namespace.useState([]);
|
|
295
|
+
const [projects, setProjects] = React2__namespace.useState([]);
|
|
296
|
+
const [error, setError] = React2__namespace.useState(null);
|
|
297
|
+
const [orgName, setOrgName] = React2__namespace.useState(null);
|
|
298
|
+
const [projectName, setProjectName] = React2__namespace.useState(null);
|
|
299
|
+
const [epoch, setEpoch] = React2__namespace.useState(0);
|
|
300
|
+
React2__namespace.useEffect(() => {
|
|
301
|
+
const ac = new AbortController();
|
|
302
|
+
let live = true;
|
|
303
|
+
void (async () => {
|
|
304
|
+
const token = await (auth.token ?? storedToken)();
|
|
305
|
+
if (!live) return;
|
|
306
|
+
if (!token) {
|
|
307
|
+
setStatus("anonymous");
|
|
308
|
+
setAccount(null);
|
|
309
|
+
setOrgs([]);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const call = { base, token, signal: ac.signal };
|
|
313
|
+
try {
|
|
314
|
+
const me = await iam.account(call);
|
|
315
|
+
if (!live) return;
|
|
316
|
+
if (!me) {
|
|
317
|
+
setStatus("anonymous");
|
|
318
|
+
setAccount(null);
|
|
319
|
+
setOrgs([]);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
setAccount(me);
|
|
323
|
+
const rows = await iam.organizations(call).catch((e) => {
|
|
324
|
+
if (e instanceof IamError && !e.unauthenticated) return [{ owner: "admin", name: me.owner }];
|
|
325
|
+
throw e;
|
|
326
|
+
});
|
|
327
|
+
if (!live) return;
|
|
328
|
+
setOrgs(rows);
|
|
329
|
+
setStatus("ready");
|
|
330
|
+
setError(null);
|
|
331
|
+
} catch (e) {
|
|
332
|
+
if (!live || ac.signal.aborted) return;
|
|
333
|
+
if (e instanceof IamError && e.unauthenticated) {
|
|
334
|
+
setStatus("anonymous");
|
|
335
|
+
setAccount(null);
|
|
336
|
+
setOrgs([]);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
setStatus("error");
|
|
340
|
+
setError(e instanceof Error ? e.message : "Could not reach IAM.");
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
return () => {
|
|
344
|
+
live = false;
|
|
345
|
+
ac.abort();
|
|
346
|
+
};
|
|
347
|
+
}, [base, auth, epoch]);
|
|
348
|
+
const org = React2__namespace.useMemo(() => {
|
|
349
|
+
if (orgs.length === 0) return null;
|
|
350
|
+
const saved = orgName ?? readScope(whiteLabel.host, "org");
|
|
351
|
+
return orgs.find((o) => o.name === saved) ?? orgs.find((o) => o.name === account?.owner) ?? orgs[0];
|
|
352
|
+
}, [orgs, orgName, account, whiteLabel.host]);
|
|
353
|
+
React2__namespace.useEffect(() => {
|
|
354
|
+
if (!org) {
|
|
355
|
+
setProjects([]);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const ac = new AbortController();
|
|
359
|
+
let live = true;
|
|
360
|
+
void (async () => {
|
|
361
|
+
const token = await (auth.token ?? storedToken)();
|
|
362
|
+
if (!live || !token) return;
|
|
363
|
+
const rows = await iam.projects({ base, token, signal: ac.signal }, org.name).catch(() => []);
|
|
364
|
+
if (live) setProjects(rows);
|
|
365
|
+
})();
|
|
366
|
+
return () => {
|
|
367
|
+
live = false;
|
|
368
|
+
ac.abort();
|
|
369
|
+
};
|
|
370
|
+
}, [base, auth, org, epoch]);
|
|
371
|
+
const project = React2__namespace.useMemo(() => {
|
|
372
|
+
if (projects.length === 0) return null;
|
|
373
|
+
const saved = projectName ?? readScope(whiteLabel.host, "project");
|
|
374
|
+
return projects.find((p) => p.name === saved) ?? projects.find((p) => p.isDefault) ?? projects[0];
|
|
375
|
+
}, [projects, projectName, whiteLabel.host]);
|
|
376
|
+
const value = React2__namespace.useMemo(() => ({
|
|
377
|
+
status,
|
|
378
|
+
account,
|
|
379
|
+
orgs,
|
|
380
|
+
org,
|
|
381
|
+
projects,
|
|
382
|
+
project,
|
|
383
|
+
error,
|
|
384
|
+
whiteLabel,
|
|
385
|
+
setOrg: (name) => {
|
|
386
|
+
writeScope(whiteLabel.host, "org", name);
|
|
387
|
+
setOrgName(name);
|
|
388
|
+
setProjectName(null);
|
|
389
|
+
},
|
|
390
|
+
setProject: (name) => {
|
|
391
|
+
writeScope(whiteLabel.host, "project", name);
|
|
392
|
+
setProjectName(name);
|
|
393
|
+
},
|
|
394
|
+
signIn: () => {
|
|
395
|
+
if (auth.signIn) return auth.signIn();
|
|
396
|
+
if (typeof window !== "undefined") window.location.assign(loginPath);
|
|
397
|
+
},
|
|
398
|
+
signOut: () => {
|
|
399
|
+
if (auth.signOut) return auth.signOut();
|
|
400
|
+
clearStoredTokens();
|
|
401
|
+
if (typeof window !== "undefined") window.location.assign(loginPath);
|
|
402
|
+
},
|
|
403
|
+
reload: () => setEpoch((n) => n + 1)
|
|
404
|
+
}), [status, account, orgs, org, projects, project, error, whiteLabel, auth, loginPath]);
|
|
405
|
+
return /* @__PURE__ */ jsxRuntime.jsx(IdentityContext.Provider, { value, children });
|
|
406
|
+
};
|
|
407
|
+
var CALLBACK_PATH = "/auth/callback";
|
|
408
|
+
function configureAuth(wl) {
|
|
409
|
+
if (typeof window === "undefined") return;
|
|
410
|
+
browser.configureIam({
|
|
411
|
+
issuer: wl.issuer,
|
|
412
|
+
clientId: wl.clientId,
|
|
413
|
+
// The SDK already defaults to `${origin}${CALLBACK_PATH}`; naming it here
|
|
414
|
+
// makes the fleet-wide invariant one readable, testable line instead of a
|
|
415
|
+
// default nobody can see.
|
|
416
|
+
redirect: `${window.location.origin}${CALLBACK_PATH}`
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function useAuth(defaultRedirect = "/") {
|
|
420
|
+
const whiteLabel = useWhiteLabel();
|
|
421
|
+
configureAuth(whiteLabel);
|
|
422
|
+
return React2__namespace.useMemo(() => ({
|
|
423
|
+
signIn: (redirect = defaultRedirect) => void browser.startLogin({ redirect }),
|
|
424
|
+
signOut: () => void browser.logout(),
|
|
425
|
+
complete: () => browser.handleCallback(),
|
|
426
|
+
whiteLabel
|
|
427
|
+
}), [whiteLabel, defaultRedirect]);
|
|
428
|
+
}
|
|
429
|
+
var AuthProvider = ({ children, redirect = "/", onSignOut }) => {
|
|
430
|
+
const auth = useAuth(redirect);
|
|
431
|
+
const identityAuth = React2__namespace.useMemo(() => ({
|
|
432
|
+
signIn: () => auth.signIn(redirect),
|
|
433
|
+
signOut: () => {
|
|
434
|
+
onSignOut?.();
|
|
435
|
+
auth.signOut();
|
|
436
|
+
}
|
|
437
|
+
}), [auth, redirect, onSignOut]);
|
|
438
|
+
return /* @__PURE__ */ jsxRuntime.jsx(IdentityProvider, { auth: identityAuth, children });
|
|
439
|
+
};
|
|
440
|
+
var AuthCallback = ({
|
|
441
|
+
onSignedIn,
|
|
442
|
+
navigate,
|
|
443
|
+
fallback = "/",
|
|
444
|
+
retryPath = "/login"
|
|
445
|
+
}) => {
|
|
446
|
+
const auth = useAuth();
|
|
447
|
+
const [error, setError] = React2__namespace.useState(null);
|
|
448
|
+
React2__namespace.useEffect(() => {
|
|
449
|
+
let live = true;
|
|
450
|
+
void (async () => {
|
|
451
|
+
try {
|
|
452
|
+
const result = await auth.complete();
|
|
453
|
+
if (!live) return;
|
|
454
|
+
try {
|
|
455
|
+
await onSignedIn?.(result);
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
if (!live) return;
|
|
459
|
+
const to = result.redirect || fallback;
|
|
460
|
+
if (navigate) navigate(to);
|
|
461
|
+
else window.location.replace(to);
|
|
462
|
+
} catch (e) {
|
|
463
|
+
if (live) setError(e instanceof Error ? e.message : "Authentication failed");
|
|
464
|
+
}
|
|
465
|
+
})();
|
|
466
|
+
return () => {
|
|
467
|
+
live = false;
|
|
468
|
+
};
|
|
469
|
+
}, []);
|
|
470
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-[50vh] items-center justify-center px-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-sm text-center", children: error ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
471
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-lg font-semibold text-[var(--color-text-error)]", children: "Sign-in failed" }),
|
|
472
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-6 text-sm text-[var(--color-text-secondary)]", children: error }),
|
|
473
|
+
/* @__PURE__ */ jsxRuntime.jsx("a", { href: retryPath, className: "text-sm underline underline-offset-4", children: "Try again" })
|
|
474
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-[var(--color-text-secondary)]", children: "Completing sign in\u2026" }) }) });
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
exports.AuthCallback = AuthCallback;
|
|
478
|
+
exports.AuthProvider = AuthProvider;
|
|
479
|
+
exports.CALLBACK_PATH = CALLBACK_PATH;
|
|
480
|
+
exports.configureAuth = configureAuth;
|
|
481
|
+
exports.useAuth = useAuth;
|
package/dist/auth.d.cts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { WhiteLabel } from './white-label.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The ONE callback route. Registered as `https://<host>/auth/callback` for
|
|
6
|
+
* every browser client in the fleet — never under `/api/`, never per app.
|
|
7
|
+
*/
|
|
8
|
+
declare const CALLBACK_PATH = "/auth/callback";
|
|
9
|
+
/** What `handleCallback` hands back. Structural, so the SDK's types stay its own. */
|
|
10
|
+
interface AuthResult {
|
|
11
|
+
readonly token: {
|
|
12
|
+
readonly accessToken: string;
|
|
13
|
+
};
|
|
14
|
+
readonly redirect: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Point `@hanzo/iam` at the issuer and client this HOST resolves to.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent per config, browser-only, and safe to call from anywhere — which
|
|
20
|
+
* is the point: no ordering rule between a provider's effect and a child's.
|
|
21
|
+
*/
|
|
22
|
+
declare function configureAuth(wl: WhiteLabel): void;
|
|
23
|
+
interface Auth {
|
|
24
|
+
/** Start the PKCE login. `redirect` is where to land afterwards. */
|
|
25
|
+
readonly signIn: (redirect?: string) => void;
|
|
26
|
+
/** RP-initiated logout + local token clear. */
|
|
27
|
+
readonly signOut: () => void;
|
|
28
|
+
/** Finish the login on the callback route. */
|
|
29
|
+
readonly complete: () => Promise<AuthResult>;
|
|
30
|
+
/** The host-resolved brand, issuer and client these verbs are bound to. */
|
|
31
|
+
readonly whiteLabel: WhiteLabel;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The three auth verbs, bound to the current host.
|
|
35
|
+
*
|
|
36
|
+
* Configuration happens during render, before any caller can use a verb, so a
|
|
37
|
+
* callback route works whether or not it sits under an `<AuthProvider>`.
|
|
38
|
+
*/
|
|
39
|
+
declare function useAuth(defaultRedirect?: string): Auth;
|
|
40
|
+
interface AuthProviderProps {
|
|
41
|
+
readonly children: React.ReactNode;
|
|
42
|
+
/** Where a successful sign-in lands. Default `/`. */
|
|
43
|
+
readonly redirect?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Anything the surface must tear down alongside the IAM session — the mirror
|
|
46
|
+
* of {@link AuthCallbackProps.onSignedIn}. A surface that mints its own
|
|
47
|
+
* downstream credential from the IAM token drops it here; the IAM session
|
|
48
|
+
* itself is never the app's to manage.
|
|
49
|
+
*/
|
|
50
|
+
readonly onSignOut?: () => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `IdentityProvider` with the credential half filled in.
|
|
54
|
+
*
|
|
55
|
+
* A surface's root becomes `<AppProvider><AuthProvider>…` and nothing else —
|
|
56
|
+
* no `configureIam`, no `startLogin`, no per-app token keys.
|
|
57
|
+
*/
|
|
58
|
+
declare const AuthProvider: ({ children, redirect, onSignOut }: AuthProviderProps) => React.ReactElement;
|
|
59
|
+
interface AuthCallbackProps {
|
|
60
|
+
/**
|
|
61
|
+
* Anything the surface must do with the fresh session before it navigates —
|
|
62
|
+
* e.g. the MPC dashboard mints its own API JWT from the IAM token. Throwing
|
|
63
|
+
* here does NOT fail the sign-in: the user is signed in, and a downstream
|
|
64
|
+
* service being unreachable is that service's problem to report.
|
|
65
|
+
*/
|
|
66
|
+
readonly onSignedIn?: (result: AuthResult) => void | Promise<void>;
|
|
67
|
+
/** Router hop. Defaults to a full replace, which needs no router. */
|
|
68
|
+
readonly navigate?: (to: string) => void;
|
|
69
|
+
/** Where to go when the callback carried no stashed redirect. */
|
|
70
|
+
readonly fallback?: string;
|
|
71
|
+
/** Sign-in entry point offered after a failure. */
|
|
72
|
+
readonly retryPath?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The ONE callback page body: verify state, exchange the code, go where the
|
|
76
|
+
* user was headed. Mount it at {@link CALLBACK_PATH}.
|
|
77
|
+
*/
|
|
78
|
+
declare const AuthCallback: ({ onSignedIn, navigate, fallback, retryPath, }: AuthCallbackProps) => React.ReactElement;
|
|
79
|
+
|
|
80
|
+
export { type Auth, AuthCallback, type AuthCallbackProps, AuthProvider, type AuthProviderProps, type AuthResult, CALLBACK_PATH, configureAuth, useAuth };
|