@byggnode/auth-client 0.0.1

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 ADDED
@@ -0,0 +1,70 @@
1
+ # `@byggnode/auth-client`
2
+
3
+ Thin Angular client for the Byggnode central IdP (`https://auth.handverksradet.no`).
4
+ Use this from product apps (HR, …). Domain logic stays in those apps.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm i @byggnode/auth-client
10
+ ```
11
+
12
+ Peer dependencies: `@angular/core`, `@angular/common`, `@angular/router` ^22.1
13
+ (same major as the auth console).
14
+
15
+ The package is ready to publish (`publishConfig.access=public`). npm publish
16
+ CI is a follow-up; from this repo:
17
+
18
+ ```bash
19
+ bun install --cwd packages/auth-client
20
+ bun run --cwd packages/auth-client build
21
+ # publish the ng-packagr output:
22
+ npm publish packages/auth-client/dist --access public
23
+ ```
24
+
25
+ ## Wire up
26
+
27
+ ```ts
28
+ import { provideAuthClient, SessionStore } from "@byggnode/auth-client";
29
+
30
+ export const appConfig = {
31
+ providers: [
32
+ provideAuthClient({
33
+ authOrigin: "https://auth.handverksradet.no",
34
+ appSlug: "handverksradet",
35
+ returnUrl: "https://hr.example/callback",
36
+ }),
37
+ ],
38
+ };
39
+ ```
40
+
41
+ ```ts
42
+ const session = inject(SessionStore);
43
+
44
+ // Hosted login on the IdP (app + return_url query)
45
+ session.redirectToLogin();
46
+
47
+ // Or send a magic link from your own form
48
+ await session.requestMagicLink(email);
49
+
50
+ // Invite a user (Auth sends mail and applies role on accept)
51
+ await session.createInvite({ email, role: "cms" });
52
+
53
+ // /callback?code=… → POST /oauth/token
54
+ await session.handleCallback(code);
55
+
56
+ if (session.can("articles:publish")) {
57
+ // …
58
+ }
59
+
60
+ const me = await session.request("/me");
61
+ await session.logout();
62
+ ```
63
+
64
+ `SessionStore.request` sends `Authorization: Bearer …`, refreshes when the
65
+ access token is inside the 60s skew window, and retries once on 401 — same
66
+ behavior as the auth console. Session tokens are stored in **localStorage** by
67
+ default (survives new tabs); only a known-invalid refresh (`refresh_expired` /
68
+ `invalid_refresh`) clears the session — network blips and 5xx do not.
69
+
70
+ Grant-dialog UI is a stub (`GrantDialog`) and is not required for v1.
@@ -0,0 +1,601 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, signal, computed, Injectable, makeEnvironmentProviders, input, output, Component } from '@angular/core';
3
+ import { Router } from '@angular/router';
4
+
5
+ const ACCESS_REFRESH_SKEW_MS = 60_000;
6
+ function jwtExpMs(accessToken) {
7
+ const parts = accessToken.split(".");
8
+ if (parts.length < 2 || !parts[1])
9
+ return null;
10
+ try {
11
+ const raw = parts[1].replaceAll("-", "+").replaceAll("_", "/");
12
+ const pad = raw.length % 4 === 0 ? "" : "=".repeat(4 - (raw.length % 4));
13
+ const json = atob(raw + pad);
14
+ const exp = JSON.parse(json).exp;
15
+ return typeof exp === "number" && Number.isFinite(exp) ? exp * 1000 : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ function accessTokenExpiresAtMs(session) {
22
+ if (typeof session.expires_at === "number" && Number.isFinite(session.expires_at)) {
23
+ return session.expires_at;
24
+ }
25
+ return jwtExpMs(session.access_token ?? "");
26
+ }
27
+ function accessTokenNeedsRefresh(expiresAtMs, now = Date.now(), skewMs = ACCESS_REFRESH_SKEW_MS) {
28
+ if (expiresAtMs == null)
29
+ return false;
30
+ return expiresAtMs - now <= skewMs;
31
+ }
32
+ function withExpiresAt(session, now = Date.now()) {
33
+ const fromJwt = jwtExpMs(session.access_token);
34
+ const fromTtl = typeof session.expires_in === "number" && session.expires_in > 0
35
+ ? now + session.expires_in * 1000
36
+ : null;
37
+ const expiresAt = session.expires_at ?? fromJwt ?? fromTtl ?? undefined;
38
+ return expiresAt == null ? session : { ...session, expires_at: expiresAt };
39
+ }
40
+
41
+ function resolveAuthClientConfig(config) {
42
+ const appSlug = config.appSlug.trim();
43
+ const returnUrl = config.returnUrl.trim();
44
+ if (!appSlug) {
45
+ throw new Error("provideAuthClient: appSlug is required");
46
+ }
47
+ if (!returnUrl) {
48
+ throw new Error("provideAuthClient: returnUrl is required");
49
+ }
50
+ return {
51
+ authOrigin: config.authOrigin.replace(/\/$/, ""),
52
+ appSlug,
53
+ returnUrl,
54
+ storageKey: config.storageKey?.trim() || "auth.session",
55
+ loginPath: config.loginPath?.trim() || "/login",
56
+ refreshSkewMs: config.refreshSkewMs ?? ACCESS_REFRESH_SKEW_MS,
57
+ expiredPath: config.expiredPath?.trim() || "/login?expired=1",
58
+ };
59
+ }
60
+
61
+ function readErrorCode(body) {
62
+ if (!body || typeof body !== "object")
63
+ return "";
64
+ const error = body.error;
65
+ return typeof error === "string" ? error : "";
66
+ }
67
+ function isUnauthorized(status, body) {
68
+ if (status === 401)
69
+ return true;
70
+ return readErrorCode(body) === "unauthorized";
71
+ }
72
+ function jsonHeaders(init = {}) {
73
+ const headers = new Headers(init.headers);
74
+ if (init.body && !headers.has("content-type")) {
75
+ headers.set("content-type", "application/json");
76
+ }
77
+ if (init.token)
78
+ headers.set("authorization", `Bearer ${init.token}`);
79
+ return headers;
80
+ }
81
+ async function parseJsonResponse(res) {
82
+ const body = (await res.json().catch(() => ({})));
83
+ return { status: res.status, body };
84
+ }
85
+ async function authJson(fetchFn, url, init = {}) {
86
+ const { token, ...rest } = init;
87
+ const res = await fetchFn(url, { ...rest, headers: jsonHeaders(init) });
88
+ return parseJsonResponse(res);
89
+ }
90
+
91
+ function createMemoryStorage() {
92
+ const map = new Map();
93
+ return {
94
+ getItem(key) {
95
+ return map.get(key) ?? null;
96
+ },
97
+ setItem(key, value) {
98
+ map.set(key, value);
99
+ },
100
+ removeItem(key) {
101
+ map.delete(key);
102
+ },
103
+ };
104
+ }
105
+ /**
106
+ * Prefer localStorage so IdP sessions survive new tabs and browser restarts.
107
+ * Fall back to in-memory storage when localStorage is unavailable (private
108
+ * mode / non-browser); sessionStorage is a last resort if localStorage throws
109
+ * but sessionStorage works.
110
+ */
111
+ /** One-shot: copy auth.session from sessionStorage → localStorage (cutover). */
112
+ function migrateSessionKeyToLocal(key = "auth.session") {
113
+ try {
114
+ if (typeof localStorage === "undefined" || typeof sessionStorage === "undefined")
115
+ return;
116
+ if (localStorage.getItem(key))
117
+ return;
118
+ const raw = sessionStorage.getItem(key);
119
+ if (!raw)
120
+ return;
121
+ localStorage.setItem(key, raw);
122
+ sessionStorage.removeItem(key);
123
+ }
124
+ catch {
125
+ // ignore quota / private mode
126
+ }
127
+ }
128
+ function defaultSessionStorage() {
129
+ try {
130
+ if (typeof localStorage !== "undefined") {
131
+ migrateSessionKeyToLocal();
132
+ return localStorage;
133
+ }
134
+ }
135
+ catch {
136
+ // private mode / non-browser
137
+ }
138
+ try {
139
+ if (typeof sessionStorage !== "undefined")
140
+ return sessionStorage;
141
+ }
142
+ catch {
143
+ // private mode / non-browser
144
+ }
145
+ return createMemoryStorage();
146
+ }
147
+ function readStoredJson(storage, key) {
148
+ try {
149
+ const raw = storage.getItem(key);
150
+ if (!raw)
151
+ return null;
152
+ return JSON.parse(raw);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function isSessionPayload(value) {
160
+ if (!value || typeof value !== "object")
161
+ return false;
162
+ const rec = value;
163
+ return (typeof rec.access_token === "string" &&
164
+ rec.access_token.length > 0 &&
165
+ typeof rec.refresh_token === "string" &&
166
+ !!rec.user &&
167
+ typeof rec.user.id === "string" &&
168
+ typeof rec.user.email === "string" &&
169
+ !!rec.app &&
170
+ typeof rec.app.slug === "string" &&
171
+ Array.isArray(rec.roles) &&
172
+ Array.isArray(rec.permissions));
173
+ }
174
+
175
+ function joinAuthUrl(origin, path) {
176
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
177
+ if (!origin)
178
+ return normalizedPath;
179
+ return `${origin.replace(/\/$/, "")}${normalizedPath}`;
180
+ }
181
+ function isHttpUrl(value) {
182
+ try {
183
+ const url = new URL(value);
184
+ return url.protocol === "http:" || url.protocol === "https:";
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }
190
+ function resolveReturnUrl(requested, fallback) {
191
+ if (requested && isHttpUrl(requested))
192
+ return requested;
193
+ return fallback;
194
+ }
195
+ function readCallbackCode(searchOrUrl) {
196
+ const raw = searchOrUrl.trim();
197
+ if (!raw)
198
+ return null;
199
+ try {
200
+ if (raw.includes("://") || raw.startsWith("/") || raw.startsWith("?")) {
201
+ const url = raw.includes("://")
202
+ ? new URL(raw)
203
+ : new URL(raw, "http://auth.local");
204
+ return url.searchParams.get("code");
205
+ }
206
+ }
207
+ catch {
208
+ // treat as a bare code below
209
+ }
210
+ if (raw.includes("=")) {
211
+ return new URLSearchParams(raw.startsWith("?") ? raw.slice(1) : raw).get("code");
212
+ }
213
+ return raw;
214
+ }
215
+ function buildLoginUrl(opts) {
216
+ const path = joinAuthUrl(opts.authOrigin, opts.loginPath ?? "/login");
217
+ const url = path.includes("://")
218
+ ? new URL(path)
219
+ : new URL(path, "http://auth.local");
220
+ url.searchParams.set("app", opts.appSlug);
221
+ url.searchParams.set("return_url", opts.returnUrl);
222
+ if (path.includes("://"))
223
+ return url.toString();
224
+ return `${url.pathname}${url.search}`;
225
+ }
226
+
227
+ const INVALID_REFRESH_ERRORS = new Set([
228
+ "invalid_refresh",
229
+ "refresh_expired",
230
+ "refresh_token_required",
231
+ "invalid_grant",
232
+ ]);
233
+ function isInvalidRefreshResponse(status, body) {
234
+ if (status === 401)
235
+ return true;
236
+ if (status !== 400)
237
+ return false;
238
+ const code = readErrorCode(body);
239
+ return INVALID_REFRESH_ERRORS.has(code);
240
+ }
241
+ class AuthClient {
242
+ config;
243
+ storage;
244
+ fetchFn;
245
+ current;
246
+ refreshInFlight = null;
247
+ constructor(config, options = {}) {
248
+ this.config = resolveAuthClientConfig(config);
249
+ this.storage = options.storage ?? defaultSessionStorage();
250
+ this.fetchFn = options.fetch ?? fetch;
251
+ this.current = this.loadFromStorage();
252
+ }
253
+ getSession() {
254
+ return this.current;
255
+ }
256
+ get token() {
257
+ return this.current?.access_token ?? "";
258
+ }
259
+ get user() {
260
+ return this.current?.user ?? null;
261
+ }
262
+ get app() {
263
+ return this.current?.app ?? null;
264
+ }
265
+ get roles() {
266
+ return this.current?.roles ?? [];
267
+ }
268
+ get permissions() {
269
+ return this.current?.permissions ?? [];
270
+ }
271
+ can(permission) {
272
+ return this.permissions.includes(permission);
273
+ }
274
+ setSession(session) {
275
+ const next = withExpiresAt(session);
276
+ this.current = next;
277
+ this.storage.setItem(this.config.storageKey, JSON.stringify(next));
278
+ return next;
279
+ }
280
+ clearSession() {
281
+ this.current = null;
282
+ this.storage.removeItem(this.config.storageKey);
283
+ }
284
+ loginUrl() {
285
+ return buildLoginUrl(this.config);
286
+ }
287
+ /** Redirect the browser to the IdP login page (`app` + `return_url`). */
288
+ redirectToLogin(assign = defaultAssign) {
289
+ assign(this.loginUrl());
290
+ }
291
+ async requestMagicLink(email, intent = "login") {
292
+ return authJson(this.fetchFn, this.url("/magic-link"), {
293
+ method: "POST",
294
+ body: JSON.stringify({
295
+ email,
296
+ app: this.config.appSlug,
297
+ return_url: this.config.returnUrl,
298
+ intent,
299
+ }),
300
+ });
301
+ }
302
+ /** Authenticated `POST /invites`. Auth owns mail + accept. */
303
+ async createInvite(input) {
304
+ return this.request("/invites", {
305
+ method: "POST",
306
+ body: JSON.stringify({
307
+ email: input.email,
308
+ role: input.role,
309
+ app: input.app ?? this.config.appSlug,
310
+ orgnr: input.orgnr,
311
+ return_url: input.return_url ?? this.config.returnUrl,
312
+ }),
313
+ });
314
+ }
315
+ async exchangeCode(code) {
316
+ const res = await authJson(this.fetchFn, this.url("/oauth/token"), {
317
+ method: "POST",
318
+ body: JSON.stringify({
319
+ grant_type: "authorization_code",
320
+ code,
321
+ app: this.config.appSlug,
322
+ }),
323
+ });
324
+ if (res.status === 200 && isSessionPayload(res.body)) {
325
+ this.setSession(res.body);
326
+ }
327
+ return res;
328
+ }
329
+ async handleCallback(codeOrSearch) {
330
+ const code = readCallbackCode(codeOrSearch);
331
+ if (!code) {
332
+ throw new Error("missing_code");
333
+ }
334
+ const res = await this.exchangeCode(code);
335
+ if (res.status !== 200 || !isSessionPayload(res.body)) {
336
+ throw new Error(res.body.error ?? "exchange_failed");
337
+ }
338
+ return this.current;
339
+ }
340
+ async logout() {
341
+ const token = this.token;
342
+ if (token) {
343
+ await authJson(this.fetchFn, this.url("/logout"), {
344
+ method: "POST",
345
+ token,
346
+ }).catch(() => undefined);
347
+ }
348
+ this.clearSession();
349
+ }
350
+ /**
351
+ * Authenticated JSON request with Bearer token, proactive refresh, and a
352
+ * single 401 retry — same shape as the auth console `SessionStore.request`.
353
+ *
354
+ * Clears the stored session only when refresh is known-invalid (expired /
355
+ * revoked refresh token). Network blips and 5xx do not log the user out.
356
+ */
357
+ async request(path, init = {}) {
358
+ const fresh = await this.ensureFreshToken();
359
+ if (!fresh) {
360
+ if (!this.token) {
361
+ return { status: 401, body: { error: "unauthorized" } };
362
+ }
363
+ // Transient refresh failure — keep session; caller can retry later.
364
+ return { status: 503, body: { error: "refresh_unavailable" } };
365
+ }
366
+ const first = await authJson(this.fetchFn, this.url(path), {
367
+ ...init,
368
+ token: this.token,
369
+ });
370
+ if (!isUnauthorized(first.status, first.body))
371
+ return first;
372
+ const renewed = await this.refreshSessionDetailed();
373
+ if (renewed === "refreshed") {
374
+ return authJson(this.fetchFn, this.url(path), {
375
+ ...init,
376
+ token: this.token,
377
+ });
378
+ }
379
+ if (renewed === "invalid") {
380
+ this.clearSession();
381
+ }
382
+ return first;
383
+ }
384
+ async ensureFreshToken() {
385
+ if (!this.token)
386
+ return false;
387
+ const expiresAt = accessTokenExpiresAtMs(this.current ?? {});
388
+ if (!accessTokenNeedsRefresh(expiresAt, Date.now(), this.config.refreshSkewMs)) {
389
+ return true;
390
+ }
391
+ const outcome = await this.refreshSessionDetailed();
392
+ if (outcome === "refreshed")
393
+ return true;
394
+ if (outcome === "invalid") {
395
+ this.clearSession();
396
+ return false;
397
+ }
398
+ // Transient: keep stored session so the caller can retry.
399
+ return false;
400
+ }
401
+ async refreshSession() {
402
+ const outcome = await this.refreshSessionDetailed();
403
+ if (outcome === "refreshed")
404
+ return true;
405
+ if (outcome === "invalid") {
406
+ this.clearSession();
407
+ }
408
+ return false;
409
+ }
410
+ async refreshSessionDetailed() {
411
+ if (this.refreshInFlight)
412
+ return this.refreshInFlight;
413
+ this.refreshInFlight = this.doRefresh().finally(() => {
414
+ this.refreshInFlight = null;
415
+ });
416
+ return this.refreshInFlight;
417
+ }
418
+ async doRefresh() {
419
+ const refreshToken = this.current?.refresh_token;
420
+ if (!refreshToken)
421
+ return "invalid";
422
+ try {
423
+ const res = await authJson(this.fetchFn, this.url("/oauth/token"), {
424
+ method: "POST",
425
+ body: JSON.stringify({
426
+ grant_type: "refresh_token",
427
+ refresh_token: refreshToken,
428
+ }),
429
+ });
430
+ if (res.status === 200 && isSessionPayload(res.body)) {
431
+ this.setSession(res.body);
432
+ return "refreshed";
433
+ }
434
+ if (isInvalidRefreshResponse(res.status, res.body)) {
435
+ return "invalid";
436
+ }
437
+ return "transient";
438
+ }
439
+ catch {
440
+ // Network error / aborted fetch — do not clear session.
441
+ return "transient";
442
+ }
443
+ }
444
+ url(path) {
445
+ return joinAuthUrl(this.config.authOrigin, path);
446
+ }
447
+ loadFromStorage() {
448
+ const raw = readStoredJson(this.storage, this.config.storageKey);
449
+ if (!raw || !isSessionPayload(raw))
450
+ return null;
451
+ return withExpiresAt(raw);
452
+ }
453
+ }
454
+ function createAuthClient(config, options) {
455
+ return new AuthClient(config, options);
456
+ }
457
+ function defaultAssign(url) {
458
+ if (typeof location !== "undefined") {
459
+ location.assign(url);
460
+ return;
461
+ }
462
+ throw new Error(`redirectToLogin: no location (${url})`);
463
+ }
464
+
465
+ const AUTH_CLIENT_CONFIG = new InjectionToken("AUTH_CLIENT_CONFIG");
466
+
467
+ class SessionStore {
468
+ client = inject(AuthClient);
469
+ router = inject(Router);
470
+ config = inject(AUTH_CLIENT_CONFIG);
471
+ session = signal(this.client.getSession(), /* @ts-ignore */
472
+ ...(ngDevMode ? [{ debugName: "session" }] : /* istanbul ignore next */ []));
473
+ token = computed(() => this.session()?.access_token ?? "", /* @ts-ignore */
474
+ ...(ngDevMode ? [{ debugName: "token" }] : /* istanbul ignore next */ []));
475
+ user = computed(() => this.session()?.user ?? null, /* @ts-ignore */
476
+ ...(ngDevMode ? [{ debugName: "user" }] : /* istanbul ignore next */ []));
477
+ app = computed(() => this.session()?.app ?? null, /* @ts-ignore */
478
+ ...(ngDevMode ? [{ debugName: "app" }] : /* istanbul ignore next */ []));
479
+ roles = computed(() => this.session()?.roles ?? [], /* @ts-ignore */
480
+ ...(ngDevMode ? [{ debugName: "roles" }] : /* istanbul ignore next */ []));
481
+ permissions = computed(() => this.session()?.permissions ?? [], /* @ts-ignore */
482
+ ...(ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []));
483
+ can(permission) {
484
+ return this.permissions().includes(permission);
485
+ }
486
+ set(session) {
487
+ this.session.set(this.client.setSession(session));
488
+ }
489
+ clear() {
490
+ this.client.clearSession();
491
+ this.session.set(null);
492
+ }
493
+ expireToLogin() {
494
+ this.clear();
495
+ void this.router.navigateByUrl(this.config.expiredPath ?? "/login?expired=1");
496
+ }
497
+ async logout() {
498
+ await this.client.logout();
499
+ this.session.set(null);
500
+ }
501
+ loginUrl() {
502
+ return this.client.loginUrl();
503
+ }
504
+ redirectToLogin() {
505
+ this.client.redirectToLogin();
506
+ }
507
+ async requestMagicLink(email, intent = "login") {
508
+ return this.client.requestMagicLink(email, intent);
509
+ }
510
+ async createInvite(input) {
511
+ const result = await this.client.createInvite(input);
512
+ this.session.set(this.client.getSession());
513
+ return result;
514
+ }
515
+ async handleCallback(codeOrSearch) {
516
+ const session = await this.client.handleCallback(codeOrSearch);
517
+ this.session.set(session);
518
+ return session;
519
+ }
520
+ async request(path, init = {}) {
521
+ const result = await this.client.request(path, init);
522
+ this.session.set(this.client.getSession());
523
+ if (result.status === 401 && !this.client.token) {
524
+ this.expireToLogin();
525
+ }
526
+ return result;
527
+ }
528
+ async ensureFreshToken() {
529
+ const ok = await this.client.ensureFreshToken();
530
+ this.session.set(this.client.getSession());
531
+ return ok;
532
+ }
533
+ async refreshSession() {
534
+ const ok = await this.client.refreshSession();
535
+ this.session.set(this.client.getSession());
536
+ return ok;
537
+ }
538
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: SessionStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
539
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: SessionStore });
540
+ }
541
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: SessionStore, decorators: [{
542
+ type: Injectable
543
+ }] });
544
+
545
+ function provideAuthClient(config) {
546
+ const resolved = resolveAuthClientConfig(config);
547
+ return makeEnvironmentProviders([
548
+ { provide: AUTH_CLIENT_CONFIG, useValue: resolved },
549
+ {
550
+ provide: AuthClient,
551
+ useFactory: () => new AuthClient(resolved, { storage: defaultSessionStorage() }),
552
+ },
553
+ SessionStore,
554
+ ]);
555
+ }
556
+
557
+ function requireAuthSession(loginUrl = "/login") {
558
+ return () => {
559
+ const session = inject(SessionStore);
560
+ const router = inject(Router);
561
+ return session.token() ? true : router.parseUrl(loginUrl);
562
+ };
563
+ }
564
+
565
+ /**
566
+ * Placeholder for a shared grant-access dialog.
567
+ * Product apps should keep their own UI in v1 — this is not blocking.
568
+ */
569
+ class GrantDialog {
570
+ open = input(false, /* @ts-ignore */
571
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
572
+ closed = output();
573
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: GrantDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
574
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: GrantDialog, isStandalone: true, selector: "jg-auth-grant-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, ngImport: i0, template: `
575
+ @if (open()) {
576
+ <p class="jg-auth-grant-dialog-stub">
577
+ Grant dialog is not implemented in @byggnode/auth-client v1.
578
+ </p>
579
+ }
580
+ `, isInline: true });
581
+ }
582
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: GrantDialog, decorators: [{
583
+ type: Component,
584
+ args: [{
585
+ selector: "jg-auth-grant-dialog",
586
+ template: `
587
+ @if (open()) {
588
+ <p class="jg-auth-grant-dialog-stub">
589
+ Grant dialog is not implemented in @byggnode/auth-client v1.
590
+ </p>
591
+ }
592
+ `,
593
+ }]
594
+ }], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
595
+
596
+ /**
597
+ * Generated bundle index. Do not edit.
598
+ */
599
+
600
+ export { ACCESS_REFRESH_SKEW_MS, AUTH_CLIENT_CONFIG, AuthClient, GrantDialog, SessionStore, accessTokenExpiresAtMs, accessTokenNeedsRefresh, authJson, buildLoginUrl, createAuthClient, createMemoryStorage, defaultSessionStorage, isHttpUrl, isSessionPayload, isUnauthorized, joinAuthUrl, jsonHeaders, jwtExpMs, parseJsonResponse, provideAuthClient, readCallbackCode, readErrorCode, readStoredJson, requireAuthSession, resolveAuthClientConfig, resolveReturnUrl, withExpiresAt };
601
+ //# sourceMappingURL=byggnode-auth-client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byggnode-auth-client.mjs","sources":["../../src/lib/session-expiry.ts","../../src/lib/config.ts","../../src/lib/http.ts","../../src/lib/storage.ts","../../src/lib/types.ts","../../src/lib/urls.ts","../../src/lib/client.ts","../../src/lib/tokens.ts","../../src/lib/session.ts","../../src/lib/provide.ts","../../src/lib/guards.ts","../../src/lib/grant-dialog.ts","../../src/byggnode-auth-client.ts"],"sourcesContent":["export const ACCESS_REFRESH_SKEW_MS = 60_000;\n\nexport function jwtExpMs(accessToken: string): number | null {\n const parts = accessToken.split(\".\");\n if (parts.length < 2 || !parts[1]) return null;\n try {\n const raw = parts[1].replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\");\n const pad = raw.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (raw.length % 4));\n const json = atob(raw + pad);\n const exp = (JSON.parse(json) as { exp?: unknown }).exp;\n return typeof exp === \"number\" && Number.isFinite(exp) ? exp * 1000 : null;\n } catch {\n return null;\n }\n}\n\nexport function accessTokenExpiresAtMs(session: {\n access_token?: string;\n expires_at?: number;\n}): number | null {\n if (typeof session.expires_at === \"number\" && Number.isFinite(session.expires_at)) {\n return session.expires_at;\n }\n return jwtExpMs(session.access_token ?? \"\");\n}\n\nexport function accessTokenNeedsRefresh(\n expiresAtMs: number | null,\n now = Date.now(),\n skewMs = ACCESS_REFRESH_SKEW_MS,\n): boolean {\n if (expiresAtMs == null) return false;\n return expiresAtMs - now <= skewMs;\n}\n\nexport function withExpiresAt<\n T extends { access_token: string; expires_in?: number; expires_at?: number },\n>(session: T, now = Date.now()): T & { expires_at?: number } {\n const fromJwt = jwtExpMs(session.access_token);\n const fromTtl =\n typeof session.expires_in === \"number\" && session.expires_in > 0\n ? now + session.expires_in * 1000\n : null;\n const expiresAt = session.expires_at ?? fromJwt ?? fromTtl ?? undefined;\n return expiresAt == null ? session : { ...session, expires_at: expiresAt };\n}\n","import { ACCESS_REFRESH_SKEW_MS } from \"./session-expiry\";\n\nexport type AuthClientConfig = {\n /** IdP origin, e.g. `https://auth.handverksradet.no`. Empty string = same origin. */\n authOrigin: string;\n /** Registered app slug sent on magic-link / login redirect / token exchange. */\n appSlug: string;\n /** Allowed callback URL; verify redirects here with `?code=`. */\n returnUrl: string;\n /** Browser storage key (localStorage by default). Default matches the auth console (`auth.session`). */\n storageKey?: string;\n /** Path on the IdP for the hosted login page. Default `/login`. */\n loginPath?: string;\n /** Refresh this many ms before access-token expiry. Default 60s. */\n refreshSkewMs?: number;\n /** Router path SessionStore uses after a failed refresh. Default `/login?expired=1`. */\n expiredPath?: string;\n};\n\nexport type ResolvedAuthClientConfig = {\n authOrigin: string;\n appSlug: string;\n returnUrl: string;\n storageKey: string;\n loginPath: string;\n refreshSkewMs: number;\n expiredPath: string;\n};\n\nexport function resolveAuthClientConfig(\n config: AuthClientConfig,\n): ResolvedAuthClientConfig {\n const appSlug = config.appSlug.trim();\n const returnUrl = config.returnUrl.trim();\n if (!appSlug) {\n throw new Error(\"provideAuthClient: appSlug is required\");\n }\n if (!returnUrl) {\n throw new Error(\"provideAuthClient: returnUrl is required\");\n }\n return {\n authOrigin: config.authOrigin.replace(/\\/$/, \"\"),\n appSlug,\n returnUrl,\n storageKey: config.storageKey?.trim() || \"auth.session\",\n loginPath: config.loginPath?.trim() || \"/login\",\n refreshSkewMs: config.refreshSkewMs ?? ACCESS_REFRESH_SKEW_MS,\n expiredPath: config.expiredPath?.trim() || \"/login?expired=1\",\n };\n}\n","import type { AuthApiResult } from \"./types\";\n\nexport function readErrorCode(body: unknown): string {\n if (!body || typeof body !== \"object\") return \"\";\n const error = (body as { error?: unknown }).error;\n return typeof error === \"string\" ? error : \"\";\n}\n\nexport function isUnauthorized(status: number, body: unknown): boolean {\n if (status === 401) return true;\n return readErrorCode(body) === \"unauthorized\";\n}\n\nexport function jsonHeaders(\n init: RequestInit & { token?: string } = {},\n): Headers {\n const headers = new Headers(init.headers);\n if (init.body && !headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/json\");\n }\n if (init.token) headers.set(\"authorization\", `Bearer ${init.token}`);\n return headers;\n}\n\nexport async function parseJsonResponse<T>(\n res: Response,\n): Promise<AuthApiResult<T>> {\n const body = (await res.json().catch(() => ({}))) as T;\n return { status: res.status, body };\n}\n\nexport type AuthFetchFn = (\n input: string,\n init?: RequestInit,\n) => Promise<Response>;\n\nexport async function authJson<T>(\n fetchFn: AuthFetchFn,\n url: string,\n init: RequestInit & { token?: string } = {},\n): Promise<AuthApiResult<T>> {\n const { token, ...rest } = init;\n const res = await fetchFn(url, { ...rest, headers: jsonHeaders(init) });\n return parseJsonResponse<T>(res);\n}\n","export type SessionStorageLike = {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n};\n\nexport function createMemoryStorage(): SessionStorageLike {\n const map = new Map<string, string>();\n return {\n getItem(key) {\n return map.get(key) ?? null;\n },\n setItem(key, value) {\n map.set(key, value);\n },\n removeItem(key) {\n map.delete(key);\n },\n };\n}\n\n/**\n * Prefer localStorage so IdP sessions survive new tabs and browser restarts.\n * Fall back to in-memory storage when localStorage is unavailable (private\n * mode / non-browser); sessionStorage is a last resort if localStorage throws\n * but sessionStorage works.\n */\n/** One-shot: copy auth.session from sessionStorage → localStorage (cutover). */\nfunction migrateSessionKeyToLocal(key = \"auth.session\"): void {\n try {\n if (typeof localStorage === \"undefined\" || typeof sessionStorage === \"undefined\") return;\n if (localStorage.getItem(key)) return;\n const raw = sessionStorage.getItem(key);\n if (!raw) return;\n localStorage.setItem(key, raw);\n sessionStorage.removeItem(key);\n } catch {\n // ignore quota / private mode\n }\n}\n\nexport function defaultSessionStorage(): SessionStorageLike {\n try {\n if (typeof localStorage !== \"undefined\") {\n migrateSessionKeyToLocal();\n return localStorage;\n }\n } catch {\n // private mode / non-browser\n }\n try {\n if (typeof sessionStorage !== \"undefined\") return sessionStorage;\n } catch {\n // private mode / non-browser\n }\n return createMemoryStorage();\n}\n\nexport function readStoredJson<T>(\n storage: SessionStorageLike,\n key: string,\n): T | null {\n try {\n const raw = storage.getItem(key);\n if (!raw) return null;\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n","export type AuthUser = {\n id: string;\n email: string;\n};\n\nexport type AppInfo = {\n slug: string;\n name: string;\n active?: boolean;\n allowed_origins?: string[];\n redirect_uris?: string[];\n};\n\nexport type RoleScope = {\n role: string;\n orgnr: string | null;\n};\n\nexport type SessionPayload = {\n access_token: string;\n refresh_token: string;\n token_type?: string;\n expires_in?: number;\n expires_at?: number;\n session_id?: string;\n user: AuthUser;\n app: { slug: string; name: string };\n roles: string[];\n permissions: string[];\n scopes: RoleScope[];\n};\n\nexport type AuthApiResult<T> = {\n status: number;\n body: T;\n};\n\nexport type MagicLinkResponse = {\n ok?: boolean;\n error?: string;\n dev_token?: string;\n};\n\nexport type InviteRequest = {\n email: string;\n role: string;\n app?: string;\n orgnr?: string;\n return_url?: string;\n};\n\nexport type InviteResponse = {\n ok?: boolean;\n error?: string;\n invite?: {\n id: string;\n email: string;\n role: string;\n app: string;\n orgnr: string | null;\n status?: string;\n expires_at: string;\n };\n dev_token?: string;\n};\n\nexport type TokenErrorBody = {\n error?: string;\n};\n\nexport type RoleRow = {\n slug: string;\n name: string;\n description?: string | null;\n permissions: string[];\n};\n\nexport function isSessionPayload(value: unknown): value is SessionPayload {\n if (!value || typeof value !== \"object\") return false;\n const rec = value as Partial<SessionPayload>;\n return (\n typeof rec.access_token === \"string\" &&\n rec.access_token.length > 0 &&\n typeof rec.refresh_token === \"string\" &&\n !!rec.user &&\n typeof rec.user.id === \"string\" &&\n typeof rec.user.email === \"string\" &&\n !!rec.app &&\n typeof rec.app.slug === \"string\" &&\n Array.isArray(rec.roles) &&\n Array.isArray(rec.permissions)\n );\n}\n","export function joinAuthUrl(origin: string, path: string): string {\n const normalizedPath = path.startsWith(\"/\") ? path : `/${path}`;\n if (!origin) return normalizedPath;\n return `${origin.replace(/\\/$/, \"\")}${normalizedPath}`;\n}\n\nexport function isHttpUrl(value: string): boolean {\n try {\n const url = new URL(value);\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n\nexport function resolveReturnUrl(\n requested: string | null | undefined,\n fallback: string,\n): string {\n if (requested && isHttpUrl(requested)) return requested;\n return fallback;\n}\n\nexport function readCallbackCode(searchOrUrl: string): string | null {\n const raw = searchOrUrl.trim();\n if (!raw) return null;\n try {\n if (raw.includes(\"://\") || raw.startsWith(\"/\") || raw.startsWith(\"?\")) {\n const url = raw.includes(\"://\")\n ? new URL(raw)\n : new URL(raw, \"http://auth.local\");\n return url.searchParams.get(\"code\");\n }\n } catch {\n // treat as a bare code below\n }\n if (raw.includes(\"=\")) {\n return new URLSearchParams(raw.startsWith(\"?\") ? raw.slice(1) : raw).get(\n \"code\",\n );\n }\n return raw;\n}\n\nexport function buildLoginUrl(opts: {\n authOrigin: string;\n appSlug: string;\n returnUrl: string;\n loginPath?: string;\n}): string {\n const path = joinAuthUrl(opts.authOrigin, opts.loginPath ?? \"/login\");\n const url = path.includes(\"://\")\n ? new URL(path)\n : new URL(path, \"http://auth.local\");\n url.searchParams.set(\"app\", opts.appSlug);\n url.searchParams.set(\"return_url\", opts.returnUrl);\n if (path.includes(\"://\")) return url.toString();\n return `${url.pathname}${url.search}`;\n}\n","import {\n resolveAuthClientConfig,\n type AuthClientConfig,\n type ResolvedAuthClientConfig,\n} from \"./config\";\nimport {\n authJson,\n isUnauthorized,\n readErrorCode,\n type AuthFetchFn,\n} from \"./http\";\nimport {\n accessTokenExpiresAtMs,\n accessTokenNeedsRefresh,\n withExpiresAt,\n} from \"./session-expiry\";\nimport {\n defaultSessionStorage,\n readStoredJson,\n type SessionStorageLike,\n} from \"./storage\";\nimport {\n isSessionPayload,\n type AuthApiResult,\n type InviteRequest,\n type InviteResponse,\n type MagicLinkResponse,\n type SessionPayload,\n type TokenErrorBody,\n} from \"./types\";\nimport { buildLoginUrl, joinAuthUrl, readCallbackCode } from \"./urls\";\n\nexport type AuthClientOptions = {\n storage?: SessionStorageLike;\n fetch?: AuthFetchFn;\n};\n\n/** Outcome of a refresh_token grant attempt. */\ntype RefreshOutcome = \"refreshed\" | \"invalid\" | \"transient\";\n\nconst INVALID_REFRESH_ERRORS = new Set([\n \"invalid_refresh\",\n \"refresh_expired\",\n \"refresh_token_required\",\n \"invalid_grant\",\n]);\n\nfunction isInvalidRefreshResponse(status: number, body: unknown): boolean {\n if (status === 401) return true;\n if (status !== 400) return false;\n const code = readErrorCode(body);\n return INVALID_REFRESH_ERRORS.has(code);\n}\n\nexport class AuthClient {\n readonly config: ResolvedAuthClientConfig;\n private readonly storage: SessionStorageLike;\n private readonly fetchFn: AuthFetchFn;\n private current: SessionPayload | null;\n private refreshInFlight: Promise<RefreshOutcome> | null = null;\n\n constructor(config: AuthClientConfig, options: AuthClientOptions = {}) {\n this.config = resolveAuthClientConfig(config);\n this.storage = options.storage ?? defaultSessionStorage();\n this.fetchFn = options.fetch ?? fetch;\n this.current = this.loadFromStorage();\n }\n\n getSession(): SessionPayload | null {\n return this.current;\n }\n\n get token(): string {\n return this.current?.access_token ?? \"\";\n }\n\n get user(): SessionPayload[\"user\"] | null {\n return this.current?.user ?? null;\n }\n\n get app(): SessionPayload[\"app\"] | null {\n return this.current?.app ?? null;\n }\n\n get roles(): string[] {\n return this.current?.roles ?? [];\n }\n\n get permissions(): string[] {\n return this.current?.permissions ?? [];\n }\n\n can(permission: string): boolean {\n return this.permissions.includes(permission);\n }\n\n setSession(session: SessionPayload): SessionPayload {\n const next = withExpiresAt(session);\n this.current = next;\n this.storage.setItem(this.config.storageKey, JSON.stringify(next));\n return next;\n }\n\n clearSession(): void {\n this.current = null;\n this.storage.removeItem(this.config.storageKey);\n }\n\n loginUrl(): string {\n return buildLoginUrl(this.config);\n }\n\n /** Redirect the browser to the IdP login page (`app` + `return_url`). */\n redirectToLogin(assign: (url: string) => void = defaultAssign): void {\n assign(this.loginUrl());\n }\n\n async requestMagicLink(\n email: string,\n intent: \"login\" | \"privat\" = \"login\",\n ): Promise<AuthApiResult<MagicLinkResponse>> {\n return authJson<MagicLinkResponse>(this.fetchFn, this.url(\"/magic-link\"), {\n method: \"POST\",\n body: JSON.stringify({\n email,\n app: this.config.appSlug,\n return_url: this.config.returnUrl,\n intent,\n }),\n });\n }\n\n /** Authenticated `POST /invites`. Auth owns mail + accept. */\n async createInvite(\n input: InviteRequest,\n ): Promise<AuthApiResult<InviteResponse>> {\n return this.request<InviteResponse>(\"/invites\", {\n method: \"POST\",\n body: JSON.stringify({\n email: input.email,\n role: input.role,\n app: input.app ?? this.config.appSlug,\n orgnr: input.orgnr,\n return_url: input.return_url ?? this.config.returnUrl,\n }),\n });\n }\n\n async exchangeCode(\n code: string,\n ): Promise<AuthApiResult<SessionPayload & TokenErrorBody>> {\n const res = await authJson<SessionPayload & TokenErrorBody>(\n this.fetchFn,\n this.url(\"/oauth/token\"),\n {\n method: \"POST\",\n body: JSON.stringify({\n grant_type: \"authorization_code\",\n code,\n app: this.config.appSlug,\n }),\n },\n );\n if (res.status === 200 && isSessionPayload(res.body)) {\n this.setSession(res.body);\n }\n return res;\n }\n\n async handleCallback(codeOrSearch: string): Promise<SessionPayload> {\n const code = readCallbackCode(codeOrSearch);\n if (!code) {\n throw new Error(\"missing_code\");\n }\n const res = await this.exchangeCode(code);\n if (res.status !== 200 || !isSessionPayload(res.body)) {\n throw new Error(res.body.error ?? \"exchange_failed\");\n }\n return this.current!;\n }\n\n async logout(): Promise<void> {\n const token = this.token;\n if (token) {\n await authJson(this.fetchFn, this.url(\"/logout\"), {\n method: \"POST\",\n token,\n }).catch(() => undefined);\n }\n this.clearSession();\n }\n\n /**\n * Authenticated JSON request with Bearer token, proactive refresh, and a\n * single 401 retry — same shape as the auth console `SessionStore.request`.\n *\n * Clears the stored session only when refresh is known-invalid (expired /\n * revoked refresh token). Network blips and 5xx do not log the user out.\n */\n async request<T>(\n path: string,\n init: RequestInit = {},\n ): Promise<AuthApiResult<T>> {\n const fresh = await this.ensureFreshToken();\n if (!fresh) {\n if (!this.token) {\n return { status: 401, body: { error: \"unauthorized\" } as T };\n }\n // Transient refresh failure — keep session; caller can retry later.\n return { status: 503, body: { error: \"refresh_unavailable\" } as T };\n }\n const first = await authJson<T>(this.fetchFn, this.url(path), {\n ...init,\n token: this.token,\n });\n if (!isUnauthorized(first.status, first.body)) return first;\n const renewed = await this.refreshSessionDetailed();\n if (renewed === \"refreshed\") {\n return authJson<T>(this.fetchFn, this.url(path), {\n ...init,\n token: this.token,\n });\n }\n if (renewed === \"invalid\") {\n this.clearSession();\n }\n return first;\n }\n\n async ensureFreshToken(): Promise<boolean> {\n if (!this.token) return false;\n const expiresAt = accessTokenExpiresAtMs(this.current ?? {});\n if (\n !accessTokenNeedsRefresh(\n expiresAt,\n Date.now(),\n this.config.refreshSkewMs,\n )\n ) {\n return true;\n }\n const outcome = await this.refreshSessionDetailed();\n if (outcome === \"refreshed\") return true;\n if (outcome === \"invalid\") {\n this.clearSession();\n return false;\n }\n // Transient: keep stored session so the caller can retry.\n return false;\n }\n\n async refreshSession(): Promise<boolean> {\n const outcome = await this.refreshSessionDetailed();\n if (outcome === \"refreshed\") return true;\n if (outcome === \"invalid\") {\n this.clearSession();\n }\n return false;\n }\n\n private async refreshSessionDetailed(): Promise<RefreshOutcome> {\n if (this.refreshInFlight) return this.refreshInFlight;\n this.refreshInFlight = this.doRefresh().finally(() => {\n this.refreshInFlight = null;\n });\n return this.refreshInFlight;\n }\n\n private async doRefresh(): Promise<RefreshOutcome> {\n const refreshToken = this.current?.refresh_token;\n if (!refreshToken) return \"invalid\";\n try {\n const res = await authJson<SessionPayload & TokenErrorBody>(\n this.fetchFn,\n this.url(\"/oauth/token\"),\n {\n method: \"POST\",\n body: JSON.stringify({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n }),\n },\n );\n if (res.status === 200 && isSessionPayload(res.body)) {\n this.setSession(res.body);\n return \"refreshed\";\n }\n if (isInvalidRefreshResponse(res.status, res.body)) {\n return \"invalid\";\n }\n return \"transient\";\n } catch {\n // Network error / aborted fetch — do not clear session.\n return \"transient\";\n }\n }\n\n private url(path: string): string {\n return joinAuthUrl(this.config.authOrigin, path);\n }\n\n private loadFromStorage(): SessionPayload | null {\n const raw = readStoredJson<SessionPayload>(\n this.storage,\n this.config.storageKey,\n );\n if (!raw || !isSessionPayload(raw)) return null;\n return withExpiresAt(raw);\n }\n}\n\nexport function createAuthClient(\n config: AuthClientConfig,\n options?: AuthClientOptions,\n): AuthClient {\n return new AuthClient(config, options);\n}\n\nfunction defaultAssign(url: string): void {\n if (typeof location !== \"undefined\") {\n location.assign(url);\n return;\n }\n throw new Error(`redirectToLogin: no location (${url})`);\n}\n","import { InjectionToken } from \"@angular/core\";\nimport type { ResolvedAuthClientConfig } from \"./config\";\n\nexport const AUTH_CLIENT_CONFIG = new InjectionToken<ResolvedAuthClientConfig>(\n \"AUTH_CLIENT_CONFIG\",\n);\n","import { Injectable, computed, inject, signal } from \"@angular/core\";\nimport { Router } from \"@angular/router\";\nimport { AuthClient } from \"./client\";\nimport { AUTH_CLIENT_CONFIG } from \"./tokens\";\nimport type {\n AuthApiResult,\n InviteRequest,\n InviteResponse,\n MagicLinkResponse,\n SessionPayload,\n} from \"./types\";\n\n@Injectable()\nexport class SessionStore {\n private readonly client = inject(AuthClient);\n private readonly router = inject(Router);\n private readonly config = inject(AUTH_CLIENT_CONFIG);\n readonly session = signal<SessionPayload | null>(this.client.getSession());\n readonly token = computed(() => this.session()?.access_token ?? \"\");\n readonly user = computed(() => this.session()?.user ?? null);\n readonly app = computed(() => this.session()?.app ?? null);\n readonly roles = computed(() => this.session()?.roles ?? []);\n readonly permissions = computed(() => this.session()?.permissions ?? []);\n\n can(permission: string): boolean {\n return this.permissions().includes(permission);\n }\n\n set(session: SessionPayload): void {\n this.session.set(this.client.setSession(session));\n }\n\n clear(): void {\n this.client.clearSession();\n this.session.set(null);\n }\n\n expireToLogin(): void {\n this.clear();\n void this.router.navigateByUrl(this.config.expiredPath ?? \"/login?expired=1\");\n }\n\n async logout(): Promise<void> {\n await this.client.logout();\n this.session.set(null);\n }\n\n loginUrl(): string {\n return this.client.loginUrl();\n }\n\n redirectToLogin(): void {\n this.client.redirectToLogin();\n }\n\n async requestMagicLink(\n email: string,\n intent: \"login\" | \"privat\" = \"login\",\n ): Promise<AuthApiResult<MagicLinkResponse>> {\n return this.client.requestMagicLink(email, intent);\n }\n\n async createInvite(\n input: InviteRequest,\n ): Promise<AuthApiResult<InviteResponse>> {\n const result = await this.client.createInvite(input);\n this.session.set(this.client.getSession());\n return result;\n }\n\n async handleCallback(codeOrSearch: string): Promise<SessionPayload> {\n const session = await this.client.handleCallback(codeOrSearch);\n this.session.set(session);\n return session;\n }\n\n async request<T>(\n path: string,\n init: RequestInit = {},\n ): Promise<AuthApiResult<T>> {\n const result = await this.client.request<T>(path, init);\n this.session.set(this.client.getSession());\n if (result.status === 401 && !this.client.token) {\n this.expireToLogin();\n }\n return result;\n }\n\n async ensureFreshToken(): Promise<boolean> {\n const ok = await this.client.ensureFreshToken();\n this.session.set(this.client.getSession());\n return ok;\n }\n\n async refreshSession(): Promise<boolean> {\n const ok = await this.client.refreshSession();\n this.session.set(this.client.getSession());\n return ok;\n }\n}\n","import { makeEnvironmentProviders, type EnvironmentProviders } from \"@angular/core\";\nimport { AuthClient } from \"./client\";\nimport { resolveAuthClientConfig, type AuthClientConfig } from \"./config\";\nimport { SessionStore } from \"./session\";\nimport { defaultSessionStorage } from \"./storage\";\nimport { AUTH_CLIENT_CONFIG } from \"./tokens\";\n\nexport function provideAuthClient(\n config: AuthClientConfig,\n): EnvironmentProviders {\n const resolved = resolveAuthClientConfig(config);\n return makeEnvironmentProviders([\n { provide: AUTH_CLIENT_CONFIG, useValue: resolved },\n {\n provide: AuthClient,\n useFactory: () =>\n new AuthClient(resolved, { storage: defaultSessionStorage() }),\n },\n SessionStore,\n ]);\n}\n","import { inject } from \"@angular/core\";\nimport { type CanActivateFn, Router } from \"@angular/router\";\nimport { SessionStore } from \"./session\";\n\nexport function requireAuthSession(loginUrl = \"/login\"): CanActivateFn {\n return () => {\n const session = inject(SessionStore);\n const router = inject(Router);\n return session.token() ? true : router.parseUrl(loginUrl);\n };\n}\n","import { Component, input, output } from \"@angular/core\";\n\n/**\n * Placeholder for a shared grant-access dialog.\n * Product apps should keep their own UI in v1 — this is not blocking.\n */\n@Component({\n selector: \"jg-auth-grant-dialog\",\n template: `\n @if (open()) {\n <p class=\"jg-auth-grant-dialog-stub\">\n Grant dialog is not implemented in @byggnode/auth-client v1.\n </p>\n }\n `,\n})\nexport class GrantDialog {\n readonly open = input(false);\n readonly closed = output<void>();\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@byggnode/auth-client",
3
+ "version": "0.0.1",
4
+ "description": "Thin Angular client for the Byggnode central auth IdP.",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Byggnode/auth.git",
11
+ "directory": "packages/auth-client"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "engines": {
17
+ "node": ">=22.22.3"
18
+ },
19
+ "peerDependencies": {
20
+ "@angular/common": "^22.1.0",
21
+ "@angular/core": "^22.1.0",
22
+ "@angular/router": "^22.1.0"
23
+ },
24
+ "dependencies": {
25
+ "tslib": "^2.8.1"
26
+ },
27
+ "module": "fesm2022/byggnode-auth-client.mjs",
28
+ "typings": "types/byggnode-auth-client.d.ts",
29
+ "exports": {
30
+ "./package.json": {
31
+ "default": "./package.json"
32
+ },
33
+ ".": {
34
+ "types": "./types/byggnode-auth-client.d.ts",
35
+ "default": "./fesm2022/byggnode-auth-client.mjs"
36
+ }
37
+ },
38
+ "type": "module"
39
+ }
@@ -0,0 +1,245 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { EnvironmentProviders, InjectionToken } from '@angular/core';
3
+ import * as _byggnode_auth_client from '@byggnode/auth-client';
4
+ import { CanActivateFn } from '@angular/router';
5
+
6
+ type AuthClientConfig = {
7
+ /** IdP origin, e.g. `https://auth.handverksradet.no`. Empty string = same origin. */
8
+ authOrigin: string;
9
+ /** Registered app slug sent on magic-link / login redirect / token exchange. */
10
+ appSlug: string;
11
+ /** Allowed callback URL; verify redirects here with `?code=`. */
12
+ returnUrl: string;
13
+ /** Browser storage key (localStorage by default). Default matches the auth console (`auth.session`). */
14
+ storageKey?: string;
15
+ /** Path on the IdP for the hosted login page. Default `/login`. */
16
+ loginPath?: string;
17
+ /** Refresh this many ms before access-token expiry. Default 60s. */
18
+ refreshSkewMs?: number;
19
+ /** Router path SessionStore uses after a failed refresh. Default `/login?expired=1`. */
20
+ expiredPath?: string;
21
+ };
22
+ type ResolvedAuthClientConfig = {
23
+ authOrigin: string;
24
+ appSlug: string;
25
+ returnUrl: string;
26
+ storageKey: string;
27
+ loginPath: string;
28
+ refreshSkewMs: number;
29
+ expiredPath: string;
30
+ };
31
+ declare function resolveAuthClientConfig(config: AuthClientConfig): ResolvedAuthClientConfig;
32
+
33
+ declare function provideAuthClient(config: AuthClientConfig): EnvironmentProviders;
34
+
35
+ declare const AUTH_CLIENT_CONFIG: InjectionToken<ResolvedAuthClientConfig>;
36
+
37
+ type AuthUser = {
38
+ id: string;
39
+ email: string;
40
+ };
41
+ type AppInfo = {
42
+ slug: string;
43
+ name: string;
44
+ active?: boolean;
45
+ allowed_origins?: string[];
46
+ redirect_uris?: string[];
47
+ };
48
+ type RoleScope = {
49
+ role: string;
50
+ orgnr: string | null;
51
+ };
52
+ type SessionPayload = {
53
+ access_token: string;
54
+ refresh_token: string;
55
+ token_type?: string;
56
+ expires_in?: number;
57
+ expires_at?: number;
58
+ session_id?: string;
59
+ user: AuthUser;
60
+ app: {
61
+ slug: string;
62
+ name: string;
63
+ };
64
+ roles: string[];
65
+ permissions: string[];
66
+ scopes: RoleScope[];
67
+ };
68
+ type AuthApiResult<T> = {
69
+ status: number;
70
+ body: T;
71
+ };
72
+ type MagicLinkResponse = {
73
+ ok?: boolean;
74
+ error?: string;
75
+ dev_token?: string;
76
+ };
77
+ type InviteRequest = {
78
+ email: string;
79
+ role: string;
80
+ app?: string;
81
+ orgnr?: string;
82
+ return_url?: string;
83
+ };
84
+ type InviteResponse = {
85
+ ok?: boolean;
86
+ error?: string;
87
+ invite?: {
88
+ id: string;
89
+ email: string;
90
+ role: string;
91
+ app: string;
92
+ orgnr: string | null;
93
+ status?: string;
94
+ expires_at: string;
95
+ };
96
+ dev_token?: string;
97
+ };
98
+ type TokenErrorBody = {
99
+ error?: string;
100
+ };
101
+ type RoleRow = {
102
+ slug: string;
103
+ name: string;
104
+ description?: string | null;
105
+ permissions: string[];
106
+ };
107
+ declare function isSessionPayload(value: unknown): value is SessionPayload;
108
+
109
+ declare class SessionStore {
110
+ private readonly client;
111
+ private readonly router;
112
+ private readonly config;
113
+ readonly session: _angular_core.WritableSignal<SessionPayload | null>;
114
+ readonly token: _angular_core.Signal<string>;
115
+ readonly user: _angular_core.Signal<_byggnode_auth_client.AuthUser | null>;
116
+ readonly app: _angular_core.Signal<{
117
+ slug: string;
118
+ name: string;
119
+ } | null>;
120
+ readonly roles: _angular_core.Signal<string[]>;
121
+ readonly permissions: _angular_core.Signal<string[]>;
122
+ can(permission: string): boolean;
123
+ set(session: SessionPayload): void;
124
+ clear(): void;
125
+ expireToLogin(): void;
126
+ logout(): Promise<void>;
127
+ loginUrl(): string;
128
+ redirectToLogin(): void;
129
+ requestMagicLink(email: string, intent?: "login" | "privat"): Promise<AuthApiResult<MagicLinkResponse>>;
130
+ createInvite(input: InviteRequest): Promise<AuthApiResult<InviteResponse>>;
131
+ handleCallback(codeOrSearch: string): Promise<SessionPayload>;
132
+ request<T>(path: string, init?: RequestInit): Promise<AuthApiResult<T>>;
133
+ ensureFreshToken(): Promise<boolean>;
134
+ refreshSession(): Promise<boolean>;
135
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SessionStore, never>;
136
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<any>;
137
+ }
138
+
139
+ declare function readErrorCode(body: unknown): string;
140
+ declare function isUnauthorized(status: number, body: unknown): boolean;
141
+ declare function jsonHeaders(init?: RequestInit & {
142
+ token?: string;
143
+ }): Headers;
144
+ declare function parseJsonResponse<T>(res: Response): Promise<AuthApiResult<T>>;
145
+ type AuthFetchFn = (input: string, init?: RequestInit) => Promise<Response>;
146
+ declare function authJson<T>(fetchFn: AuthFetchFn, url: string, init?: RequestInit & {
147
+ token?: string;
148
+ }): Promise<AuthApiResult<T>>;
149
+
150
+ type SessionStorageLike = {
151
+ getItem(key: string): string | null;
152
+ setItem(key: string, value: string): void;
153
+ removeItem(key: string): void;
154
+ };
155
+ declare function createMemoryStorage(): SessionStorageLike;
156
+ declare function defaultSessionStorage(): SessionStorageLike;
157
+ declare function readStoredJson<T>(storage: SessionStorageLike, key: string): T | null;
158
+
159
+ type AuthClientOptions = {
160
+ storage?: SessionStorageLike;
161
+ fetch?: AuthFetchFn;
162
+ };
163
+ declare class AuthClient {
164
+ readonly config: ResolvedAuthClientConfig;
165
+ private readonly storage;
166
+ private readonly fetchFn;
167
+ private current;
168
+ private refreshInFlight;
169
+ constructor(config: AuthClientConfig, options?: AuthClientOptions);
170
+ getSession(): SessionPayload | null;
171
+ get token(): string;
172
+ get user(): SessionPayload["user"] | null;
173
+ get app(): SessionPayload["app"] | null;
174
+ get roles(): string[];
175
+ get permissions(): string[];
176
+ can(permission: string): boolean;
177
+ setSession(session: SessionPayload): SessionPayload;
178
+ clearSession(): void;
179
+ loginUrl(): string;
180
+ /** Redirect the browser to the IdP login page (`app` + `return_url`). */
181
+ redirectToLogin(assign?: (url: string) => void): void;
182
+ requestMagicLink(email: string, intent?: "login" | "privat"): Promise<AuthApiResult<MagicLinkResponse>>;
183
+ /** Authenticated `POST /invites`. Auth owns mail + accept. */
184
+ createInvite(input: InviteRequest): Promise<AuthApiResult<InviteResponse>>;
185
+ exchangeCode(code: string): Promise<AuthApiResult<SessionPayload & TokenErrorBody>>;
186
+ handleCallback(codeOrSearch: string): Promise<SessionPayload>;
187
+ logout(): Promise<void>;
188
+ /**
189
+ * Authenticated JSON request with Bearer token, proactive refresh, and a
190
+ * single 401 retry — same shape as the auth console `SessionStore.request`.
191
+ *
192
+ * Clears the stored session only when refresh is known-invalid (expired /
193
+ * revoked refresh token). Network blips and 5xx do not log the user out.
194
+ */
195
+ request<T>(path: string, init?: RequestInit): Promise<AuthApiResult<T>>;
196
+ ensureFreshToken(): Promise<boolean>;
197
+ refreshSession(): Promise<boolean>;
198
+ private refreshSessionDetailed;
199
+ private doRefresh;
200
+ private url;
201
+ private loadFromStorage;
202
+ }
203
+ declare function createAuthClient(config: AuthClientConfig, options?: AuthClientOptions): AuthClient;
204
+
205
+ declare function requireAuthSession(loginUrl?: string): CanActivateFn;
206
+
207
+ /**
208
+ * Placeholder for a shared grant-access dialog.
209
+ * Product apps should keep their own UI in v1 — this is not blocking.
210
+ */
211
+ declare class GrantDialog {
212
+ readonly open: _angular_core.InputSignal<boolean>;
213
+ readonly closed: _angular_core.OutputEmitterRef<void>;
214
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<GrantDialog, never>;
215
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<GrantDialog, "jg-auth-grant-dialog", never, { "open": { "alias": "open"; "required": false; "isSignal": true; }; }, { "closed": "closed"; }, never, never, true, never>;
216
+ }
217
+
218
+ declare const ACCESS_REFRESH_SKEW_MS = 60000;
219
+ declare function jwtExpMs(accessToken: string): number | null;
220
+ declare function accessTokenExpiresAtMs(session: {
221
+ access_token?: string;
222
+ expires_at?: number;
223
+ }): number | null;
224
+ declare function accessTokenNeedsRefresh(expiresAtMs: number | null, now?: number, skewMs?: number): boolean;
225
+ declare function withExpiresAt<T extends {
226
+ access_token: string;
227
+ expires_in?: number;
228
+ expires_at?: number;
229
+ }>(session: T, now?: number): T & {
230
+ expires_at?: number;
231
+ };
232
+
233
+ declare function joinAuthUrl(origin: string, path: string): string;
234
+ declare function isHttpUrl(value: string): boolean;
235
+ declare function resolveReturnUrl(requested: string | null | undefined, fallback: string): string;
236
+ declare function readCallbackCode(searchOrUrl: string): string | null;
237
+ declare function buildLoginUrl(opts: {
238
+ authOrigin: string;
239
+ appSlug: string;
240
+ returnUrl: string;
241
+ loginPath?: string;
242
+ }): string;
243
+
244
+ export { ACCESS_REFRESH_SKEW_MS, AUTH_CLIENT_CONFIG, AuthClient, GrantDialog, SessionStore, accessTokenExpiresAtMs, accessTokenNeedsRefresh, authJson, buildLoginUrl, createAuthClient, createMemoryStorage, defaultSessionStorage, isHttpUrl, isSessionPayload, isUnauthorized, joinAuthUrl, jsonHeaders, jwtExpMs, parseJsonResponse, provideAuthClient, readCallbackCode, readErrorCode, readStoredJson, requireAuthSession, resolveAuthClientConfig, resolveReturnUrl, withExpiresAt };
245
+ export type { AppInfo, AuthApiResult, AuthClientConfig, AuthFetchFn, AuthUser, InviteRequest, InviteResponse, MagicLinkResponse, ResolvedAuthClientConfig, RoleRow, RoleScope, SessionPayload, SessionStorageLike, TokenErrorBody };