@authdog/angular 0.2.0
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 +117 -0
- package/dist/index.d.mts +192 -0
- package/dist/index.d.ts +192 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# `@authdog/angular`
|
|
2
|
+
|
|
3
|
+
**Authdog SDK for Angular** — a root-provided auth service backed by signals, a
|
|
4
|
+
functional HTTP interceptor, and a route guard. Integrates natively with
|
|
5
|
+
Angular's dependency injection and `HttpClient`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @authdog/angular
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @authdog/angular
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Your public key (`pk_…`) is available in the [Authdog dashboard](https://authdog.com).
|
|
18
|
+
It is safe to expose to the browser.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
Register the SDK in your standalone application config:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { ApplicationConfig } from "@angular/core";
|
|
26
|
+
import { provideHttpClient, withInterceptors } from "@angular/common/http";
|
|
27
|
+
import { provideAuthdog, authdogInterceptor } from "@authdog/angular";
|
|
28
|
+
|
|
29
|
+
export const appConfig: ApplicationConfig = {
|
|
30
|
+
providers: [
|
|
31
|
+
provideAuthdog({ publicKey: "pk_xxxxxxxxxxxxxxxx" }),
|
|
32
|
+
// Attaches `Authorization: Bearer <token>` to outgoing requests.
|
|
33
|
+
provideHttpClient(withInterceptors([authdogInterceptor])),
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Inject `AuthdogService` anywhere. Its state is exposed as **signals**, so it
|
|
41
|
+
works seamlessly in templates and `computed()`s:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Component, inject } from "@angular/core";
|
|
45
|
+
import { AuthdogService } from "@authdog/angular";
|
|
46
|
+
|
|
47
|
+
@Component({
|
|
48
|
+
selector: "app-profile",
|
|
49
|
+
standalone: true,
|
|
50
|
+
template: `
|
|
51
|
+
@if (auth.isLoading()) {
|
|
52
|
+
<p>Loading…</p>
|
|
53
|
+
} @else if (auth.token()) {
|
|
54
|
+
<p>Signed in</p>
|
|
55
|
+
<button (click)="auth.signOut()">Sign out</button>
|
|
56
|
+
} @else {
|
|
57
|
+
<button (click)="auth.signIn()">Sign in</button>
|
|
58
|
+
<button (click)="auth.signUp()">Sign up</button>
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
})
|
|
62
|
+
export class ProfileComponent {
|
|
63
|
+
readonly auth = inject(AuthdogService);
|
|
64
|
+
|
|
65
|
+
async ngOnInit() {
|
|
66
|
+
await this.auth.fetchUser();
|
|
67
|
+
console.log(this.auth.user());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Signals & methods
|
|
73
|
+
|
|
74
|
+
| Member | Type | Description |
|
|
75
|
+
| ------------------ | -------------------------- | ------------------------------------------------- |
|
|
76
|
+
| `token` | `Signal<string \| null>` | Current bearer token. |
|
|
77
|
+
| `isLoading` | `Signal<boolean>` | True during the initial bootstrap. |
|
|
78
|
+
| `isAuthenticated` | `Signal<boolean>` | True when a token **and** a user are present. |
|
|
79
|
+
| `user` | `Signal<unknown>` | Last fetched userinfo payload. |
|
|
80
|
+
| `error` | `Signal<Error \| null>` | Last sign-in / fetch error. |
|
|
81
|
+
| `signIn(pk?, url?)`| `void` | Redirect to hosted sign-in. |
|
|
82
|
+
| `signUp(pk?, url?)`| `void` | Redirect to hosted sign-up (`prompt=signup`). |
|
|
83
|
+
| `signOut()` | `void` | Clear the session and redirect to `/logout`. |
|
|
84
|
+
| `fetchUser(pk?)` | `Promise<unknown>` | Load the current user from `userinfo`. |
|
|
85
|
+
|
|
86
|
+
`publicKey` defaults to the value passed to `provideAuthdog`, so the argument is
|
|
87
|
+
optional in most calls.
|
|
88
|
+
|
|
89
|
+
### Route guard
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { Routes } from "@angular/router";
|
|
93
|
+
import { authdogGuard } from "@authdog/angular";
|
|
94
|
+
|
|
95
|
+
export const routes: Routes = [
|
|
96
|
+
{ path: "dashboard", component: DashboardComponent, canActivate: [authdogGuard] },
|
|
97
|
+
];
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> ⚠️ **`authdogGuard` is presentational / UX only — it is _not_ a security
|
|
101
|
+
> boundary.** It runs in the browser and is trivially bypassable. Every
|
|
102
|
+
> protected operation must be independently enforced server-side; the API
|
|
103
|
+
> behind a guarded route must validate the session on every request.
|
|
104
|
+
|
|
105
|
+
## How it works
|
|
106
|
+
|
|
107
|
+
On startup the service reads a `?token=` value from the URL, validates it
|
|
108
|
+
against the JWT pattern **before** persisting it to `localStorage`, then strips
|
|
109
|
+
it from the address bar via `history.replaceState`. Otherwise it restores any
|
|
110
|
+
previously stored token. All `window` / `localStorage` access is guarded so the
|
|
111
|
+
service is inert under Angular Universal (SSR). Public keys are decoded through
|
|
112
|
+
the hardened `@authdog/node-commons` parser, which enforces a trusted
|
|
113
|
+
identity-host allowlist (SSRF / token-exfiltration protection).
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { InjectionToken, EnvironmentProviders, Signal } from '@angular/core';
|
|
2
|
+
import { HttpInterceptorFn } from '@angular/common/http';
|
|
3
|
+
import { CanActivateFn } from '@angular/router';
|
|
4
|
+
import { PublicKeyPayload } from '@authdog/node-commons';
|
|
5
|
+
export { PublicKeyPayload } from '@authdog/node-commons';
|
|
6
|
+
|
|
7
|
+
interface AuthdogConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Authdog public key (`pk_…`). Safe to expose to the browser. Used as the
|
|
10
|
+
* `client_id` and to derive the identity host / environment for OIDC flows.
|
|
11
|
+
*/
|
|
12
|
+
publicKey: string;
|
|
13
|
+
/**
|
|
14
|
+
* Path the route guard redirects to when the user is not authenticated.
|
|
15
|
+
* Defaults to `/`.
|
|
16
|
+
*/
|
|
17
|
+
loginPath?: string;
|
|
18
|
+
}
|
|
19
|
+
/** DI token carrying the SDK configuration provided via `provideAuthdog`. */
|
|
20
|
+
declare const AUTHDOG_CONFIG: InjectionToken<AuthdogConfig>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wires the Authdog SDK into a standalone Angular application.
|
|
24
|
+
*
|
|
25
|
+
* Add to `ApplicationConfig.providers`:
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* export const appConfig: ApplicationConfig = {
|
|
29
|
+
* providers: [
|
|
30
|
+
* provideAuthdog({ publicKey: environment.authdogPublicKey }),
|
|
31
|
+
* provideHttpClient(withInterceptors([authdogInterceptor])),
|
|
32
|
+
* ],
|
|
33
|
+
* };
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* Note: the HTTP interceptor is registered by the consumer via
|
|
37
|
+
* `withInterceptors([authdogInterceptor])` so it composes with the app's own
|
|
38
|
+
* `provideHttpClient` setup rather than forcing a particular configuration.
|
|
39
|
+
*/
|
|
40
|
+
declare const provideAuthdog: (config: AuthdogConfig) => EnvironmentProviders;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Root-provided service that owns the client-side auth state.
|
|
44
|
+
*
|
|
45
|
+
* State is exposed as readonly signals so templates and `computed`s react to
|
|
46
|
+
* changes automatically. The service is browser-first: every `window` /
|
|
47
|
+
* `localStorage` access is guarded so it is inert under Angular Universal
|
|
48
|
+
* (SSR), where it simply reports "not loading, no token".
|
|
49
|
+
*/
|
|
50
|
+
declare class AuthdogService {
|
|
51
|
+
private readonly config;
|
|
52
|
+
private readonly _token;
|
|
53
|
+
private readonly _isLoading;
|
|
54
|
+
private readonly _user;
|
|
55
|
+
private readonly _error;
|
|
56
|
+
/** Current bearer token, or `null` when signed out. */
|
|
57
|
+
readonly token: Signal<string | null>;
|
|
58
|
+
/** True until the initial token-from-URL / localStorage bootstrap finishes. */
|
|
59
|
+
readonly isLoading: Signal<boolean>;
|
|
60
|
+
/** The last fetched userinfo `user` payload, or `null`. */
|
|
61
|
+
readonly user: Signal<unknown>;
|
|
62
|
+
/** The last error raised by a sign-in / fetch operation, or `null`. */
|
|
63
|
+
readonly error: Signal<Error | null>;
|
|
64
|
+
/** True when a token is present AND a user has been loaded. */
|
|
65
|
+
readonly isAuthenticated: Signal<boolean>;
|
|
66
|
+
constructor();
|
|
67
|
+
/**
|
|
68
|
+
* Reads a token from the URL (`?token=`) or localStorage. A URL token is
|
|
69
|
+
* validated against the JWT pattern BEFORE it is persisted, so arbitrary
|
|
70
|
+
* attacker-supplied query data is never written to storage, and is stripped
|
|
71
|
+
* from the address bar via `history.replaceState` regardless of validity.
|
|
72
|
+
*/
|
|
73
|
+
private bootstrap;
|
|
74
|
+
/** Imperatively set (or clear) the in-memory + persisted token. */
|
|
75
|
+
setToken(token: string | null): void;
|
|
76
|
+
/** Redirect the browser to the hosted sign-in page. */
|
|
77
|
+
signIn(publicKey?: string, redirectUrl?: string): void;
|
|
78
|
+
/** Redirect the browser to the hosted sign-up page (`prompt=signup`). */
|
|
79
|
+
signUp(publicKey?: string, redirectUrl?: string): void;
|
|
80
|
+
/** Clear the session locally and redirect to the logout endpoint. */
|
|
81
|
+
signOut(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Fetches the current user from the identity host's userinfo endpoint and
|
|
84
|
+
* stores it on the `user` signal. Returns `null` when there is no token.
|
|
85
|
+
*/
|
|
86
|
+
fetchUser(publicKey?: string): Promise<unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Functional HTTP interceptor that attaches `Authorization: Bearer <token>`
|
|
91
|
+
* to outgoing requests when a session token is present. Register it with
|
|
92
|
+
* `provideAuthdog()` or directly via `provideHttpClient(withInterceptors([authdogInterceptor]))`.
|
|
93
|
+
*
|
|
94
|
+
* Requests that already carry an `Authorization` header are left untouched.
|
|
95
|
+
*/
|
|
96
|
+
declare const authdogInterceptor: HttpInterceptorFn;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* ⚠️ PRESENTATIONAL / UX ONLY — NOT A SECURITY BOUNDARY.
|
|
100
|
+
*
|
|
101
|
+
* This guard prevents an unauthenticated user from *navigating* to a route in
|
|
102
|
+
* the SPA so they see a login redirect instead of a broken page. It runs
|
|
103
|
+
* entirely in the browser and is therefore trivially bypassable by any client
|
|
104
|
+
* (devtools, crafted requests, a patched bundle). It MUST NOT be relied on to
|
|
105
|
+
* protect data or actions.
|
|
106
|
+
*
|
|
107
|
+
* Every protected operation MUST be independently enforced server-side: the
|
|
108
|
+
* API behind the route has to validate the session on each request regardless
|
|
109
|
+
* of what this guard decides.
|
|
110
|
+
*/
|
|
111
|
+
declare const authdogGuard: CanActivateFn;
|
|
112
|
+
|
|
113
|
+
/** Shared localStorage key for the persisted token. */
|
|
114
|
+
declare const TOKEN_STORAGE_KEY = "token";
|
|
115
|
+
/** JWT shape: three base64url segments separated by dots. */
|
|
116
|
+
declare const JWT_PATTERN: RegExp;
|
|
117
|
+
declare const getTokenFromUri: (url: string) => string | null;
|
|
118
|
+
interface IFetchUserData {
|
|
119
|
+
user: {
|
|
120
|
+
id: string;
|
|
121
|
+
environmentId: string;
|
|
122
|
+
externalId: string;
|
|
123
|
+
userName: string;
|
|
124
|
+
displayName: string;
|
|
125
|
+
nickName: string;
|
|
126
|
+
profileUrl: string;
|
|
127
|
+
title: string;
|
|
128
|
+
userType: string;
|
|
129
|
+
preferredLanguage: string | null;
|
|
130
|
+
locale: string | null;
|
|
131
|
+
timezone: string | null;
|
|
132
|
+
active: boolean;
|
|
133
|
+
provider: string;
|
|
134
|
+
lastLogin: string;
|
|
135
|
+
createdAt: string;
|
|
136
|
+
updatedAt: string;
|
|
137
|
+
names: {
|
|
138
|
+
id: string;
|
|
139
|
+
userId: string;
|
|
140
|
+
formatted: string | null;
|
|
141
|
+
familyName: string;
|
|
142
|
+
givenName: string;
|
|
143
|
+
middleName: string | null;
|
|
144
|
+
honorificPrefix: string | null;
|
|
145
|
+
honorificSuffix: string | null;
|
|
146
|
+
createdAt: string;
|
|
147
|
+
updatedAt: string;
|
|
148
|
+
};
|
|
149
|
+
addresses: [];
|
|
150
|
+
emails: {
|
|
151
|
+
value: string;
|
|
152
|
+
primary: boolean;
|
|
153
|
+
type: string;
|
|
154
|
+
}[];
|
|
155
|
+
phoneNumbers: [];
|
|
156
|
+
ims: [];
|
|
157
|
+
photos: {
|
|
158
|
+
value: string;
|
|
159
|
+
type: string;
|
|
160
|
+
}[];
|
|
161
|
+
};
|
|
162
|
+
meta: {
|
|
163
|
+
code: number;
|
|
164
|
+
message: string;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Fetches user data from the identity host's OIDC `userinfo` endpoint. The
|
|
169
|
+
* identity host is decoded through the hardened public-key parser, which
|
|
170
|
+
* enforces the trusted-host allowlist before the bearer token is ever sent.
|
|
171
|
+
*/
|
|
172
|
+
declare const fetchUserData: (publicKey: string, token: string) => Promise<IFetchUserData | null>;
|
|
173
|
+
/**
|
|
174
|
+
* Builds the OIDC authorize URL for a sign-in or sign-up redirect. The public
|
|
175
|
+
* key is decoded through the hardened parser (no raw base64/JSON) so an
|
|
176
|
+
* untrusted identity host can never be used as the redirect target.
|
|
177
|
+
*/
|
|
178
|
+
declare const buildAuthorizeUrl: (publicKey: string, redirectUri: string, options?: {
|
|
179
|
+
signup?: boolean;
|
|
180
|
+
}) => string;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Decodes and validates an Authdog public key. Delegates to the hardened
|
|
184
|
+
* shared parser in @authdog/node-commons, which validates the payload and
|
|
185
|
+
* enforces a trusted identity-host allowlist (SSRF / token-exfiltration
|
|
186
|
+
* protection) rather than blindly decoding base64/JSON.
|
|
187
|
+
*/
|
|
188
|
+
declare const getPublicKeyPayload: (publicKey: string) => PublicKeyPayload;
|
|
189
|
+
/** Lightweight, fast-fail shape check before the full decode. */
|
|
190
|
+
declare const validatePublicKey: (publicKey: string) => void;
|
|
191
|
+
|
|
192
|
+
export { AUTHDOG_CONFIG, type AuthdogConfig, AuthdogService, type IFetchUserData, JWT_PATTERN, TOKEN_STORAGE_KEY, authdogGuard, authdogInterceptor, buildAuthorizeUrl, fetchUserData, getPublicKeyPayload, getTokenFromUri, provideAuthdog, validatePublicKey };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { InjectionToken, EnvironmentProviders, Signal } from '@angular/core';
|
|
2
|
+
import { HttpInterceptorFn } from '@angular/common/http';
|
|
3
|
+
import { CanActivateFn } from '@angular/router';
|
|
4
|
+
import { PublicKeyPayload } from '@authdog/node-commons';
|
|
5
|
+
export { PublicKeyPayload } from '@authdog/node-commons';
|
|
6
|
+
|
|
7
|
+
interface AuthdogConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Authdog public key (`pk_…`). Safe to expose to the browser. Used as the
|
|
10
|
+
* `client_id` and to derive the identity host / environment for OIDC flows.
|
|
11
|
+
*/
|
|
12
|
+
publicKey: string;
|
|
13
|
+
/**
|
|
14
|
+
* Path the route guard redirects to when the user is not authenticated.
|
|
15
|
+
* Defaults to `/`.
|
|
16
|
+
*/
|
|
17
|
+
loginPath?: string;
|
|
18
|
+
}
|
|
19
|
+
/** DI token carrying the SDK configuration provided via `provideAuthdog`. */
|
|
20
|
+
declare const AUTHDOG_CONFIG: InjectionToken<AuthdogConfig>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wires the Authdog SDK into a standalone Angular application.
|
|
24
|
+
*
|
|
25
|
+
* Add to `ApplicationConfig.providers`:
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* export const appConfig: ApplicationConfig = {
|
|
29
|
+
* providers: [
|
|
30
|
+
* provideAuthdog({ publicKey: environment.authdogPublicKey }),
|
|
31
|
+
* provideHttpClient(withInterceptors([authdogInterceptor])),
|
|
32
|
+
* ],
|
|
33
|
+
* };
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* Note: the HTTP interceptor is registered by the consumer via
|
|
37
|
+
* `withInterceptors([authdogInterceptor])` so it composes with the app's own
|
|
38
|
+
* `provideHttpClient` setup rather than forcing a particular configuration.
|
|
39
|
+
*/
|
|
40
|
+
declare const provideAuthdog: (config: AuthdogConfig) => EnvironmentProviders;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Root-provided service that owns the client-side auth state.
|
|
44
|
+
*
|
|
45
|
+
* State is exposed as readonly signals so templates and `computed`s react to
|
|
46
|
+
* changes automatically. The service is browser-first: every `window` /
|
|
47
|
+
* `localStorage` access is guarded so it is inert under Angular Universal
|
|
48
|
+
* (SSR), where it simply reports "not loading, no token".
|
|
49
|
+
*/
|
|
50
|
+
declare class AuthdogService {
|
|
51
|
+
private readonly config;
|
|
52
|
+
private readonly _token;
|
|
53
|
+
private readonly _isLoading;
|
|
54
|
+
private readonly _user;
|
|
55
|
+
private readonly _error;
|
|
56
|
+
/** Current bearer token, or `null` when signed out. */
|
|
57
|
+
readonly token: Signal<string | null>;
|
|
58
|
+
/** True until the initial token-from-URL / localStorage bootstrap finishes. */
|
|
59
|
+
readonly isLoading: Signal<boolean>;
|
|
60
|
+
/** The last fetched userinfo `user` payload, or `null`. */
|
|
61
|
+
readonly user: Signal<unknown>;
|
|
62
|
+
/** The last error raised by a sign-in / fetch operation, or `null`. */
|
|
63
|
+
readonly error: Signal<Error | null>;
|
|
64
|
+
/** True when a token is present AND a user has been loaded. */
|
|
65
|
+
readonly isAuthenticated: Signal<boolean>;
|
|
66
|
+
constructor();
|
|
67
|
+
/**
|
|
68
|
+
* Reads a token from the URL (`?token=`) or localStorage. A URL token is
|
|
69
|
+
* validated against the JWT pattern BEFORE it is persisted, so arbitrary
|
|
70
|
+
* attacker-supplied query data is never written to storage, and is stripped
|
|
71
|
+
* from the address bar via `history.replaceState` regardless of validity.
|
|
72
|
+
*/
|
|
73
|
+
private bootstrap;
|
|
74
|
+
/** Imperatively set (or clear) the in-memory + persisted token. */
|
|
75
|
+
setToken(token: string | null): void;
|
|
76
|
+
/** Redirect the browser to the hosted sign-in page. */
|
|
77
|
+
signIn(publicKey?: string, redirectUrl?: string): void;
|
|
78
|
+
/** Redirect the browser to the hosted sign-up page (`prompt=signup`). */
|
|
79
|
+
signUp(publicKey?: string, redirectUrl?: string): void;
|
|
80
|
+
/** Clear the session locally and redirect to the logout endpoint. */
|
|
81
|
+
signOut(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Fetches the current user from the identity host's userinfo endpoint and
|
|
84
|
+
* stores it on the `user` signal. Returns `null` when there is no token.
|
|
85
|
+
*/
|
|
86
|
+
fetchUser(publicKey?: string): Promise<unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Functional HTTP interceptor that attaches `Authorization: Bearer <token>`
|
|
91
|
+
* to outgoing requests when a session token is present. Register it with
|
|
92
|
+
* `provideAuthdog()` or directly via `provideHttpClient(withInterceptors([authdogInterceptor]))`.
|
|
93
|
+
*
|
|
94
|
+
* Requests that already carry an `Authorization` header are left untouched.
|
|
95
|
+
*/
|
|
96
|
+
declare const authdogInterceptor: HttpInterceptorFn;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* ⚠️ PRESENTATIONAL / UX ONLY — NOT A SECURITY BOUNDARY.
|
|
100
|
+
*
|
|
101
|
+
* This guard prevents an unauthenticated user from *navigating* to a route in
|
|
102
|
+
* the SPA so they see a login redirect instead of a broken page. It runs
|
|
103
|
+
* entirely in the browser and is therefore trivially bypassable by any client
|
|
104
|
+
* (devtools, crafted requests, a patched bundle). It MUST NOT be relied on to
|
|
105
|
+
* protect data or actions.
|
|
106
|
+
*
|
|
107
|
+
* Every protected operation MUST be independently enforced server-side: the
|
|
108
|
+
* API behind the route has to validate the session on each request regardless
|
|
109
|
+
* of what this guard decides.
|
|
110
|
+
*/
|
|
111
|
+
declare const authdogGuard: CanActivateFn;
|
|
112
|
+
|
|
113
|
+
/** Shared localStorage key for the persisted token. */
|
|
114
|
+
declare const TOKEN_STORAGE_KEY = "token";
|
|
115
|
+
/** JWT shape: three base64url segments separated by dots. */
|
|
116
|
+
declare const JWT_PATTERN: RegExp;
|
|
117
|
+
declare const getTokenFromUri: (url: string) => string | null;
|
|
118
|
+
interface IFetchUserData {
|
|
119
|
+
user: {
|
|
120
|
+
id: string;
|
|
121
|
+
environmentId: string;
|
|
122
|
+
externalId: string;
|
|
123
|
+
userName: string;
|
|
124
|
+
displayName: string;
|
|
125
|
+
nickName: string;
|
|
126
|
+
profileUrl: string;
|
|
127
|
+
title: string;
|
|
128
|
+
userType: string;
|
|
129
|
+
preferredLanguage: string | null;
|
|
130
|
+
locale: string | null;
|
|
131
|
+
timezone: string | null;
|
|
132
|
+
active: boolean;
|
|
133
|
+
provider: string;
|
|
134
|
+
lastLogin: string;
|
|
135
|
+
createdAt: string;
|
|
136
|
+
updatedAt: string;
|
|
137
|
+
names: {
|
|
138
|
+
id: string;
|
|
139
|
+
userId: string;
|
|
140
|
+
formatted: string | null;
|
|
141
|
+
familyName: string;
|
|
142
|
+
givenName: string;
|
|
143
|
+
middleName: string | null;
|
|
144
|
+
honorificPrefix: string | null;
|
|
145
|
+
honorificSuffix: string | null;
|
|
146
|
+
createdAt: string;
|
|
147
|
+
updatedAt: string;
|
|
148
|
+
};
|
|
149
|
+
addresses: [];
|
|
150
|
+
emails: {
|
|
151
|
+
value: string;
|
|
152
|
+
primary: boolean;
|
|
153
|
+
type: string;
|
|
154
|
+
}[];
|
|
155
|
+
phoneNumbers: [];
|
|
156
|
+
ims: [];
|
|
157
|
+
photos: {
|
|
158
|
+
value: string;
|
|
159
|
+
type: string;
|
|
160
|
+
}[];
|
|
161
|
+
};
|
|
162
|
+
meta: {
|
|
163
|
+
code: number;
|
|
164
|
+
message: string;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Fetches user data from the identity host's OIDC `userinfo` endpoint. The
|
|
169
|
+
* identity host is decoded through the hardened public-key parser, which
|
|
170
|
+
* enforces the trusted-host allowlist before the bearer token is ever sent.
|
|
171
|
+
*/
|
|
172
|
+
declare const fetchUserData: (publicKey: string, token: string) => Promise<IFetchUserData | null>;
|
|
173
|
+
/**
|
|
174
|
+
* Builds the OIDC authorize URL for a sign-in or sign-up redirect. The public
|
|
175
|
+
* key is decoded through the hardened parser (no raw base64/JSON) so an
|
|
176
|
+
* untrusted identity host can never be used as the redirect target.
|
|
177
|
+
*/
|
|
178
|
+
declare const buildAuthorizeUrl: (publicKey: string, redirectUri: string, options?: {
|
|
179
|
+
signup?: boolean;
|
|
180
|
+
}) => string;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Decodes and validates an Authdog public key. Delegates to the hardened
|
|
184
|
+
* shared parser in @authdog/node-commons, which validates the payload and
|
|
185
|
+
* enforces a trusted identity-host allowlist (SSRF / token-exfiltration
|
|
186
|
+
* protection) rather than blindly decoding base64/JSON.
|
|
187
|
+
*/
|
|
188
|
+
declare const getPublicKeyPayload: (publicKey: string) => PublicKeyPayload;
|
|
189
|
+
/** Lightweight, fast-fail shape check before the full decode. */
|
|
190
|
+
declare const validatePublicKey: (publicKey: string) => void;
|
|
191
|
+
|
|
192
|
+
export { AUTHDOG_CONFIG, type AuthdogConfig, AuthdogService, type IFetchUserData, JWT_PATTERN, TOKEN_STORAGE_KEY, authdogGuard, authdogInterceptor, buildAuthorizeUrl, fetchUserData, getPublicKeyPayload, getTokenFromUri, provideAuthdog, validatePublicKey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var ot=Object.create;var f=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var nt=Object.getOwnPropertyNames;var it=Object.prototype.hasOwnProperty;var L=(t,r)=>(r=Symbol[t])?r:Symbol.for("Symbol."+t),v=t=>{throw TypeError(t)};var st=(t,r,e)=>r in t?f(t,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[r]=e;var R=(t,r)=>f(t,"name",{value:r,configurable:!0});var at=(t,r)=>{for(var e in r)f(t,e,{get:r[e],enumerable:!0})},lt=(t,r,e,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of nt(r))!it.call(t,o)&&o!==e&&f(t,o,{get:()=>r[o],enumerable:!(n=H(r,o))||n.enumerable});return t};var ut=t=>lt(f({},"__esModule",{value:!0}),t);var j=t=>[,,,ot(t?.[L("metadata")]??null)],$=["class","method","getter","setter","accessor","field","value","get","set"],_=t=>t!==void 0&&typeof t!="function"?v("Function expected"):t,ct=(t,r,e,n,o)=>({kind:$[t],name:r,metadata:n,addInitializer:s=>e._?v("Already initialized"):o.push(_(s||null))}),dt=(t,r)=>st(r,L("metadata"),t[3]),W=(t,r,e,n)=>{for(var o=0,s=t[r>>1],p=s&&s.length;o<p;o++)r&1?s[o].call(e):n=s[o].call(e,n);return n},B=(t,r,e,n,o,s)=>{var p,u,G,y,U,i=r&7,x=!!(r&8),g=!!(r&16),E=i>3?t.length+1:i?x?1:2:0,S=$[i+5],D=i>3&&(t[E-1]=[]),et=t[E]||(t[E]=[]),c=i&&(!g&&!x&&(o=o.prototype),i<5&&(i>3||!g)&&H(i<4?o:{get[e](){return C(this,s)},set[e](a){return z(this,s,a)}},e));i?g&&i<4&&R(s,(i>2?"set ":i>1?"get ":"")+e):R(o,e);for(var O=n.length-1;O>=0;O--)y=ct(i,e,G={},t[3],et),i&&(y.static=x,y.private=g,U=y.access={has:g?a=>gt(o,a):a=>e in a},i^3&&(U.get=g?a=>(i^1?C:pt)(a,o,i^4?s:c.get):a=>a[e]),i>2&&(U.set=g?(a,K)=>z(a,o,K,i^4?s:c.set):(a,K)=>a[e]=K)),u=(0,n[O])(i?i<4?g?s:c[S]:i>4?void 0:{get:c.get,set:c.set}:o,y),G._=1,i^4||u===void 0?_(u)&&(i>4?D.unshift(u):i?g?s=u:c[S]=u:o=u):typeof u!="object"||u===null?v("Object expected"):(_(p=u.get)&&(c.get=p),_(p=u.set)&&(c.set=p),_(p=u.init)&&D.unshift(p));return i||dt(t,o),c&&f(o,e,c),g?i^4?s:c:o};var N=(t,r,e)=>r.has(t)||v("Cannot "+e),gt=(t,r)=>Object(r)!==r?v('Cannot use the "in" operator on this value'):t.has(r),C=(t,r,e)=>(N(t,r,"read from private field"),e?e.call(t):r.get(t));var z=(t,r,e,n)=>(N(t,r,"write to private field"),n?n.call(t,e):r.set(t,e),e),pt=(t,r,e)=>(N(t,r,"access private method"),e);var ht={};at(ht,{AUTHDOG_CONFIG:()=>h,AuthdogService:()=>l,JWT_PATTERN:()=>T,TOKEN_STORAGE_KEY:()=>m,authdogGuard:()=>rt,authdogInterceptor:()=>q,buildAuthorizeUrl:()=>b,fetchUserData:()=>k,getPublicKeyPayload:()=>P,getTokenFromUri:()=>Z,provideAuthdog:()=>Q,validatePublicKey:()=>A});module.exports=ut(ht);var M=require("@angular/core");var J=require("@angular/core"),h=new J.InjectionToken("AUTHDOG_CONFIG");var d=require("@angular/core");var Y=require("@authdog/node-commons"),P=t=>(0,Y.validateAndParsePublicKey)(t),A=t=>{if(!t)throw new Error("Public key is not defined");if(!t.startsWith("pk_"))throw new Error("Invalid public key")};var m="token",T=/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,Z=t=>new URL(t).searchParams.get("token"),k=async(t,r)=>{A(t);let e=P(t),n=await fetch(`${e.identityHost}/oidc/${e.environmentId}/userinfo`,{headers:{authorization:`Bearer ${r}`}});if(!n.ok)throw new Error("Failed to fetch user info");return await n.json()},b=(t,r,e={})=>{A(t);let n=P(t),o=new URL(`${n.identityHost}/oidc/${n.environmentId}/authorize`);return o.searchParams.set("client_id",t),o.searchParams.set("response_type","code"),o.searchParams.set("scope","openid profile email"),o.searchParams.set("redirect_uri",r),e.signup&&o.searchParams.set("prompt","signup"),o.toString()};var w=()=>typeof window<"u",V,F;V=[(0,d.Injectable)({providedIn:"root"})];var l=class{config=(0,d.inject)(h);_token=(0,d.signal)(null);_isLoading=(0,d.signal)(!0);_user=(0,d.signal)(null);_error=(0,d.signal)(null);token=this._token.asReadonly();isLoading=this._isLoading.asReadonly();user=this._user.asReadonly();error=this._error.asReadonly();isAuthenticated=(0,d.computed)(()=>!!this._token()&&!!this._user());constructor(){this.bootstrap()}bootstrap(){if(!w()){this._isLoading.set(!1);return}let r=new URL(window.location.href),e=r.searchParams.get("token");if(e&&(r.searchParams.delete("token"),window.history.replaceState({},document.title,r.toString()),T.test(e))){localStorage.setItem(m,e),this._token.set(e),this._isLoading.set(!1);return}let n=localStorage.getItem(m);n&&this._token.set(n),this._isLoading.set(!1)}setToken(r){this._token.set(r),w()&&(r?localStorage.setItem(m,r):localStorage.removeItem(m))}signIn(r=this.config.publicKey,e){if(w()){this._error.set(null);try{window.location.href=b(r,e||window.location.origin)}catch(n){this._error.set(n)}}}signUp(r=this.config.publicKey,e){if(w()){this._error.set(null);try{window.location.href=b(r,e||window.location.origin,{signup:!0})}catch(n){this._error.set(n)}}}signOut(){this.setToken(null),this._user.set(null),w()&&(window.location.href="/logout")}async fetchUser(r=this.config.publicKey){let e=this._token();if(!e)return null;this._error.set(null);try{let o=(await k(r,e))?.user??null;return this._user.set(o),o}catch(n){return this._error.set(n),null}}};F=j(null),l=B(F,0,"AuthdogService",V,l),W(F,1,l);var Q=t=>(0,M.makeEnvironmentProviders)([{provide:h,useValue:t},l]);var X=require("@angular/core");var q=(t,r)=>{let n=(0,X.inject)(l).token();return!n||t.headers.has("Authorization")?r(t):r(t.clone({setHeaders:{Authorization:`Bearer ${n}`}}))};var I=require("@angular/core"),tt=require("@angular/router");var rt=()=>{let t=(0,I.inject)(l),r=(0,I.inject)(tt.Router),e=(0,I.inject)(h);return t.token()?!0:r.parseUrl(e.loginPath??"/")};0&&(module.exports={AUTHDOG_CONFIG,AuthdogService,JWT_PATTERN,TOKEN_STORAGE_KEY,authdogGuard,authdogInterceptor,buildAuthorizeUrl,fetchUserData,getPublicKeyPayload,getTokenFromUri,provideAuthdog,validatePublicKey});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/provider.ts","../src/tokens.ts","../src/service.ts","../src/commons.ts","../src/session.ts","../src/interceptor.ts","../src/guard.ts"],"sourcesContent":["export { provideAuthdog } from \"./provider\";\nexport { AuthdogService } from \"./service\";\nexport { authdogInterceptor } from \"./interceptor\";\nexport { authdogGuard } from \"./guard\";\nexport { AUTHDOG_CONFIG, type AuthdogConfig } from \"./tokens\";\nexport {\n fetchUserData,\n buildAuthorizeUrl,\n getTokenFromUri,\n TOKEN_STORAGE_KEY,\n JWT_PATTERN,\n type IFetchUserData,\n} from \"./session\";\nexport {\n getPublicKeyPayload,\n validatePublicKey,\n type PublicKeyPayload,\n} from \"./commons\";\n","import {\n type EnvironmentProviders,\n type Provider,\n makeEnvironmentProviders,\n} from \"@angular/core\";\nimport { AUTHDOG_CONFIG, type AuthdogConfig } from \"./tokens\";\nimport { AuthdogService } from \"./service\";\n\n/**\n * Wires the Authdog SDK into a standalone Angular application.\n *\n * Add to `ApplicationConfig.providers`:\n *\n * ```ts\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * provideAuthdog({ publicKey: environment.authdogPublicKey }),\n * provideHttpClient(withInterceptors([authdogInterceptor])),\n * ],\n * };\n * ```\n *\n * Note: the HTTP interceptor is registered by the consumer via\n * `withInterceptors([authdogInterceptor])` so it composes with the app's own\n * `provideHttpClient` setup rather than forcing a particular configuration.\n */\nexport const provideAuthdog = (\n config: AuthdogConfig,\n): EnvironmentProviders => {\n const providers: Provider[] = [\n { provide: AUTHDOG_CONFIG, useValue: config },\n AuthdogService,\n ];\n\n return makeEnvironmentProviders(providers);\n};\n","import { InjectionToken } from \"@angular/core\";\n\nexport interface AuthdogConfig {\n /**\n * Authdog public key (`pk_…`). Safe to expose to the browser. Used as the\n * `client_id` and to derive the identity host / environment for OIDC flows.\n */\n publicKey: string;\n\n /**\n * Path the route guard redirects to when the user is not authenticated.\n * Defaults to `/`.\n */\n loginPath?: string;\n}\n\n/** DI token carrying the SDK configuration provided via `provideAuthdog`. */\nexport const AUTHDOG_CONFIG = new InjectionToken<AuthdogConfig>(\n \"AUTHDOG_CONFIG\",\n);\n","import {\n Injectable,\n computed,\n inject,\n signal,\n type Signal,\n} from \"@angular/core\";\nimport { AUTHDOG_CONFIG } from \"./tokens\";\nimport {\n JWT_PATTERN,\n TOKEN_STORAGE_KEY,\n buildAuthorizeUrl,\n fetchUserData,\n} from \"./session\";\n\nconst isBrowser = (): boolean => typeof window !== \"undefined\";\n\n/**\n * Root-provided service that owns the client-side auth state.\n *\n * State is exposed as readonly signals so templates and `computed`s react to\n * changes automatically. The service is browser-first: every `window` /\n * `localStorage` access is guarded so it is inert under Angular Universal\n * (SSR), where it simply reports \"not loading, no token\".\n */\n@Injectable({ providedIn: \"root\" })\nexport class AuthdogService {\n private readonly config = inject(AUTHDOG_CONFIG);\n\n private readonly _token = signal<string | null>(null);\n private readonly _isLoading = signal<boolean>(true);\n private readonly _user = signal<unknown>(null);\n private readonly _error = signal<Error | null>(null);\n\n /** Current bearer token, or `null` when signed out. */\n readonly token: Signal<string | null> = this._token.asReadonly();\n /** True until the initial token-from-URL / localStorage bootstrap finishes. */\n readonly isLoading: Signal<boolean> = this._isLoading.asReadonly();\n /** The last fetched userinfo `user` payload, or `null`. */\n readonly user: Signal<unknown> = this._user.asReadonly();\n /** The last error raised by a sign-in / fetch operation, or `null`. */\n readonly error: Signal<Error | null> = this._error.asReadonly();\n\n /** True when a token is present AND a user has been loaded. */\n readonly isAuthenticated: Signal<boolean> = computed(\n () => !!this._token() && !!this._user(),\n );\n\n constructor() {\n this.bootstrap();\n }\n\n /**\n * Reads a token from the URL (`?token=`) or localStorage. A URL token is\n * validated against the JWT pattern BEFORE it is persisted, so arbitrary\n * attacker-supplied query data is never written to storage, and is stripped\n * from the address bar via `history.replaceState` regardless of validity.\n */\n private bootstrap(): void {\n if (!isBrowser()) {\n this._isLoading.set(false);\n return;\n }\n\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(\"token\");\n\n if (urlToken) {\n // Remove the token from the URL without a reload, valid or not.\n url.searchParams.delete(\"token\");\n window.history.replaceState({}, document.title, url.toString());\n\n // Only persist values that look like a JWT.\n if (JWT_PATTERN.test(urlToken)) {\n localStorage.setItem(TOKEN_STORAGE_KEY, urlToken);\n this._token.set(urlToken);\n this._isLoading.set(false);\n return;\n }\n }\n\n const existingToken = localStorage.getItem(TOKEN_STORAGE_KEY);\n if (existingToken) {\n this._token.set(existingToken);\n }\n\n this._isLoading.set(false);\n }\n\n /** Imperatively set (or clear) the in-memory + persisted token. */\n setToken(token: string | null): void {\n this._token.set(token);\n if (!isBrowser()) return;\n\n if (token) {\n localStorage.setItem(TOKEN_STORAGE_KEY, token);\n } else {\n localStorage.removeItem(TOKEN_STORAGE_KEY);\n }\n }\n\n /** Redirect the browser to the hosted sign-in page. */\n signIn(publicKey: string = this.config.publicKey, redirectUrl?: string): void {\n if (!isBrowser()) return;\n this._error.set(null);\n try {\n window.location.href = buildAuthorizeUrl(\n publicKey,\n redirectUrl || window.location.origin,\n );\n } catch (err) {\n this._error.set(err as Error);\n }\n }\n\n /** Redirect the browser to the hosted sign-up page (`prompt=signup`). */\n signUp(publicKey: string = this.config.publicKey, redirectUrl?: string): void {\n if (!isBrowser()) return;\n this._error.set(null);\n try {\n window.location.href = buildAuthorizeUrl(\n publicKey,\n redirectUrl || window.location.origin,\n { signup: true },\n );\n } catch (err) {\n this._error.set(err as Error);\n }\n }\n\n /** Clear the session locally and redirect to the logout endpoint. */\n signOut(): void {\n this.setToken(null);\n this._user.set(null);\n if (isBrowser()) {\n window.location.href = \"/logout\";\n }\n }\n\n /**\n * Fetches the current user from the identity host's userinfo endpoint and\n * stores it on the `user` signal. Returns `null` when there is no token.\n */\n async fetchUser(\n publicKey: string = this.config.publicKey,\n ): Promise<unknown> {\n const token = this._token();\n if (!token) {\n return null;\n }\n\n this._error.set(null);\n try {\n const userData = await fetchUserData(publicKey, token);\n const user = userData?.user ?? null;\n this._user.set(user);\n return user;\n } catch (err) {\n this._error.set(err as Error);\n return null;\n }\n }\n}\n","import {\n validateAndParsePublicKey,\n type PublicKeyPayload,\n} from \"@authdog/node-commons\";\n\nexport type { PublicKeyPayload };\n\n/**\n * Decodes and validates an Authdog public key. Delegates to the hardened\n * shared parser in @authdog/node-commons, which validates the payload and\n * enforces a trusted identity-host allowlist (SSRF / token-exfiltration\n * protection) rather than blindly decoding base64/JSON.\n */\nexport const getPublicKeyPayload = (publicKey: string): PublicKeyPayload => {\n return validateAndParsePublicKey(publicKey);\n};\n\n/** Lightweight, fast-fail shape check before the full decode. */\nexport const validatePublicKey = (publicKey: string): void => {\n if (!publicKey) {\n throw new Error(\"Public key is not defined\");\n }\n\n if (!publicKey.startsWith(\"pk_\")) {\n throw new Error(\"Invalid public key\");\n }\n};\n","import { getPublicKeyPayload, validatePublicKey } from \"./commons\";\n\n/** Shared localStorage key for the persisted token. */\nexport const TOKEN_STORAGE_KEY = \"token\";\n\n/** JWT shape: three base64url segments separated by dots. */\nexport const JWT_PATTERN = /^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/;\n\nexport const getTokenFromUri = (url: string): string | null => {\n return new URL(url).searchParams.get(\"token\");\n};\n\nexport interface IFetchUserData {\n user: {\n id: string;\n environmentId: string;\n externalId: string;\n userName: string;\n displayName: string;\n nickName: string;\n profileUrl: string;\n title: string;\n userType: string;\n preferredLanguage: string | null;\n locale: string | null;\n timezone: string | null;\n active: boolean;\n provider: string;\n lastLogin: string;\n createdAt: string;\n updatedAt: string;\n names: {\n id: string;\n userId: string;\n formatted: string | null;\n familyName: string;\n givenName: string;\n middleName: string | null;\n honorificPrefix: string | null;\n honorificSuffix: string | null;\n createdAt: string;\n updatedAt: string;\n };\n addresses: [];\n emails: {\n value: string;\n primary: boolean;\n type: string;\n }[];\n phoneNumbers: [];\n ims: [];\n photos: {\n value: string;\n type: string;\n }[];\n };\n meta: {\n code: number;\n message: string;\n };\n}\n\n/**\n * Fetches user data from the identity host's OIDC `userinfo` endpoint. The\n * identity host is decoded through the hardened public-key parser, which\n * enforces the trusted-host allowlist before the bearer token is ever sent.\n */\nexport const fetchUserData = async (\n publicKey: string,\n token: string,\n): Promise<IFetchUserData | null> => {\n validatePublicKey(publicKey);\n const publicKeyObj = getPublicKeyPayload(publicKey);\n\n const userData = await fetch(\n `${publicKeyObj.identityHost}/oidc/${publicKeyObj.environmentId}/userinfo`,\n {\n headers: {\n authorization: `Bearer ${token}`,\n },\n },\n );\n\n if (!userData.ok) {\n throw new Error(\"Failed to fetch user info\");\n }\n\n return (await userData.json()) as IFetchUserData;\n};\n\n/**\n * Builds the OIDC authorize URL for a sign-in or sign-up redirect. The public\n * key is decoded through the hardened parser (no raw base64/JSON) so an\n * untrusted identity host can never be used as the redirect target.\n */\nexport const buildAuthorizeUrl = (\n publicKey: string,\n redirectUri: string,\n options: { signup?: boolean } = {},\n): string => {\n validatePublicKey(publicKey);\n const publicKeyObj = getPublicKeyPayload(publicKey);\n\n const authUrl = new URL(\n `${publicKeyObj.identityHost}/oidc/${publicKeyObj.environmentId}/authorize`,\n );\n authUrl.searchParams.set(\"client_id\", publicKey);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"scope\", \"openid profile email\");\n authUrl.searchParams.set(\"redirect_uri\", redirectUri);\n\n if (options.signup) {\n authUrl.searchParams.set(\"prompt\", \"signup\");\n }\n\n return authUrl.toString();\n};\n","import { inject } from \"@angular/core\";\nimport type { HttpInterceptorFn } from \"@angular/common/http\";\nimport { AuthdogService } from \"./service\";\n\n/**\n * Functional HTTP interceptor that attaches `Authorization: Bearer <token>`\n * to outgoing requests when a session token is present. Register it with\n * `provideAuthdog()` or directly via `provideHttpClient(withInterceptors([authdogInterceptor]))`.\n *\n * Requests that already carry an `Authorization` header are left untouched.\n */\nexport const authdogInterceptor: HttpInterceptorFn = (req, next) => {\n const auth = inject(AuthdogService);\n const token = auth.token();\n\n if (!token || req.headers.has(\"Authorization\")) {\n return next(req);\n }\n\n return next(\n req.clone({\n setHeaders: { Authorization: `Bearer ${token}` },\n }),\n );\n};\n","import { inject } from \"@angular/core\";\nimport { Router, type CanActivateFn, type UrlTree } from \"@angular/router\";\nimport { AuthdogService } from \"./service\";\nimport { AUTHDOG_CONFIG } from \"./tokens\";\n\n/**\n * ⚠️ PRESENTATIONAL / UX ONLY — NOT A SECURITY BOUNDARY.\n *\n * This guard prevents an unauthenticated user from *navigating* to a route in\n * the SPA so they see a login redirect instead of a broken page. It runs\n * entirely in the browser and is therefore trivially bypassable by any client\n * (devtools, crafted requests, a patched bundle). It MUST NOT be relied on to\n * protect data or actions.\n *\n * Every protected operation MUST be independently enforced server-side: the\n * API behind the route has to validate the session on each request regardless\n * of what this guard decides.\n */\nexport const authdogGuard: CanActivateFn = (): boolean | UrlTree => {\n const auth = inject(AuthdogService);\n const router = inject(Router);\n const config = inject(AUTHDOG_CONFIG);\n\n // A token is sufficient for *navigation* — fetching the user is async and\n // we don't want to block routing on a network round-trip.\n if (auth.token()) {\n return true;\n }\n\n return router.parseUrl(config.loginPath ?? \"/\");\n};\n"],"mappings":"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","names":["index_exports","__export","AUTHDOG_CONFIG","AuthdogService","JWT_PATTERN","TOKEN_STORAGE_KEY","authdogGuard","authdogInterceptor","buildAuthorizeUrl","fetchUserData","getPublicKeyPayload","getTokenFromUri","provideAuthdog","validatePublicKey","__toCommonJS","import_core","import_core","AUTHDOG_CONFIG","import_core","import_node_commons","getPublicKeyPayload","publicKey","validatePublicKey","TOKEN_STORAGE_KEY","JWT_PATTERN","getTokenFromUri","url","fetchUserData","publicKey","token","validatePublicKey","publicKeyObj","getPublicKeyPayload","userData","buildAuthorizeUrl","redirectUri","options","authUrl","isBrowser","_AuthdogService_decorators","_init","AuthdogService","AUTHDOG_CONFIG","url","urlToken","JWT_PATTERN","TOKEN_STORAGE_KEY","existingToken","token","publicKey","redirectUrl","buildAuthorizeUrl","err","user","fetchUserData","__decoratorStart","__decorateElement","__runInitializers","provideAuthdog","config","AUTHDOG_CONFIG","AuthdogService","import_core","authdogInterceptor","req","next","token","AuthdogService","import_core","import_router","authdogGuard","auth","AuthdogService","router","config","AUTHDOG_CONFIG"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var Y=Object.create;var x=Object.defineProperty;var Z=Object.getOwnPropertyDescriptor;var H=(t,r)=>(r=Symbol[t])?r:Symbol.for("Symbol."+t),y=t=>{throw TypeError(t)};var V=(t,r,e)=>r in t?x(t,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[r]=e;var R=(t,r)=>x(t,"name",{value:r,configurable:!0});var L=t=>[,,,Y(t?.[H("metadata")]??null)],j=["class","method","getter","setter","accessor","field","value","get","set"],f=t=>t!==void 0&&typeof t!="function"?y("Function expected"):t,M=(t,r,e,o,n)=>({kind:j[t],name:r,metadata:o,addInitializer:s=>e._?y("Already initialized"):n.push(f(s||null))}),Q=(t,r)=>V(r,H("metadata"),t[3]),$=(t,r,e,o)=>{for(var n=0,s=t[r>>1],g=s&&s.length;n<g;n++)r&1?s[n].call(e):o=s[n].call(e,o);return o},W=(t,r,e,o,n,s)=>{var g,l,G,m,w,i=r&7,T=!!(r&8),d=!!(r&16),k=i>3?t.length+1:i?T?1:2:0,S=j[i+5],D=i>3&&(t[k-1]=[]),J=t[k]||(t[k]=[]),c=i&&(!d&&!T&&(n=n.prototype),i<5&&(i>3||!d)&&Z(i<4?n:{get[e](){return C(this,s)},set[e](a){return z(this,s,a)}},e));i?d&&i<4&&R(s,(i>2?"set ":i>1?"get ":"")+e):R(n,e);for(var I=o.length-1;I>=0;I--)m=M(i,e,G={},t[3],J),i&&(m.static=T,m.private=d,w=m.access={has:d?a=>X(n,a):a=>e in a},i^3&&(w.get=d?a=>(i^1?C:q)(a,n,i^4?s:c.get):a=>a[e]),i>2&&(w.set=d?(a,U)=>z(a,n,U,i^4?s:c.set):(a,U)=>a[e]=U)),l=(0,o[I])(i?i<4?d?s:c[S]:i>4?void 0:{get:c.get,set:c.set}:n,m),G._=1,i^4||l===void 0?f(l)&&(i>4?D.unshift(l):i?d?s=l:c[S]=l:n=l):typeof l!="object"||l===null?y("Object expected"):(f(g=l.get)&&(c.get=g),f(g=l.set)&&(c.set=g),f(g=l.init)&&D.unshift(g));return i||Q(t,n),c&&x(n,e,c),d?i^4?s:c:n};var E=(t,r,e)=>r.has(t)||y("Cannot "+e),X=(t,r)=>Object(r)!==r?y('Cannot use the "in" operator on this value'):t.has(r),C=(t,r,e)=>(E(t,r,"read from private field"),e?e.call(t):r.get(t));var z=(t,r,e,o)=>(E(t,r,"write to private field"),o?o.call(t,e):r.set(t,e),e),q=(t,r,e)=>(E(t,r,"access private method"),e);import{makeEnvironmentProviders as st}from"@angular/core";import{InjectionToken as tt}from"@angular/core";var p=new tt("AUTHDOG_CONFIG");import{Injectable as ot,computed as nt,inject as it,signal as b}from"@angular/core";import{validateAndParsePublicKey as rt}from"@authdog/node-commons";var v=t=>rt(t),P=t=>{if(!t)throw new Error("Public key is not defined");if(!t.startsWith("pk_"))throw new Error("Invalid public key")};var h="token",O=/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,et=t=>new URL(t).searchParams.get("token"),K=async(t,r)=>{P(t);let e=v(t),o=await fetch(`${e.identityHost}/oidc/${e.environmentId}/userinfo`,{headers:{authorization:`Bearer ${r}`}});if(!o.ok)throw new Error("Failed to fetch user info");return await o.json()},A=(t,r,e={})=>{P(t);let o=v(t),n=new URL(`${o.identityHost}/oidc/${o.environmentId}/authorize`);return n.searchParams.set("client_id",t),n.searchParams.set("response_type","code"),n.searchParams.set("scope","openid profile email"),n.searchParams.set("redirect_uri",r),e.signup&&n.searchParams.set("prompt","signup"),n.toString()};var _=()=>typeof window<"u",B,N;B=[ot({providedIn:"root"})];var u=class{config=it(p);_token=b(null);_isLoading=b(!0);_user=b(null);_error=b(null);token=this._token.asReadonly();isLoading=this._isLoading.asReadonly();user=this._user.asReadonly();error=this._error.asReadonly();isAuthenticated=nt(()=>!!this._token()&&!!this._user());constructor(){this.bootstrap()}bootstrap(){if(!_()){this._isLoading.set(!1);return}let r=new URL(window.location.href),e=r.searchParams.get("token");if(e&&(r.searchParams.delete("token"),window.history.replaceState({},document.title,r.toString()),O.test(e))){localStorage.setItem(h,e),this._token.set(e),this._isLoading.set(!1);return}let o=localStorage.getItem(h);o&&this._token.set(o),this._isLoading.set(!1)}setToken(r){this._token.set(r),_()&&(r?localStorage.setItem(h,r):localStorage.removeItem(h))}signIn(r=this.config.publicKey,e){if(_()){this._error.set(null);try{window.location.href=A(r,e||window.location.origin)}catch(o){this._error.set(o)}}}signUp(r=this.config.publicKey,e){if(_()){this._error.set(null);try{window.location.href=A(r,e||window.location.origin,{signup:!0})}catch(o){this._error.set(o)}}}signOut(){this.setToken(null),this._user.set(null),_()&&(window.location.href="/logout")}async fetchUser(r=this.config.publicKey){let e=this._token();if(!e)return null;this._error.set(null);try{let n=(await K(r,e))?.user??null;return this._user.set(n),n}catch(o){return this._error.set(o),null}}};N=L(null),u=W(N,0,"AuthdogService",B,u),$(N,1,u);var at=t=>st([{provide:p,useValue:t},u]);import{inject as lt}from"@angular/core";var ut=(t,r)=>{let o=lt(u).token();return!o||t.headers.has("Authorization")?r(t):r(t.clone({setHeaders:{Authorization:`Bearer ${o}`}}))};import{inject as F}from"@angular/core";import{Router as ct}from"@angular/router";var dt=()=>{let t=F(u),r=F(ct),e=F(p);return t.token()?!0:r.parseUrl(e.loginPath??"/")};export{p as AUTHDOG_CONFIG,u as AuthdogService,O as JWT_PATTERN,h as TOKEN_STORAGE_KEY,dt as authdogGuard,ut as authdogInterceptor,A as buildAuthorizeUrl,K as fetchUserData,v as getPublicKeyPayload,et as getTokenFromUri,at as provideAuthdog,P as validatePublicKey};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider.ts","../src/tokens.ts","../src/service.ts","../src/commons.ts","../src/session.ts","../src/interceptor.ts","../src/guard.ts"],"sourcesContent":["import {\n type EnvironmentProviders,\n type Provider,\n makeEnvironmentProviders,\n} from \"@angular/core\";\nimport { AUTHDOG_CONFIG, type AuthdogConfig } from \"./tokens\";\nimport { AuthdogService } from \"./service\";\n\n/**\n * Wires the Authdog SDK into a standalone Angular application.\n *\n * Add to `ApplicationConfig.providers`:\n *\n * ```ts\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * provideAuthdog({ publicKey: environment.authdogPublicKey }),\n * provideHttpClient(withInterceptors([authdogInterceptor])),\n * ],\n * };\n * ```\n *\n * Note: the HTTP interceptor is registered by the consumer via\n * `withInterceptors([authdogInterceptor])` so it composes with the app's own\n * `provideHttpClient` setup rather than forcing a particular configuration.\n */\nexport const provideAuthdog = (\n config: AuthdogConfig,\n): EnvironmentProviders => {\n const providers: Provider[] = [\n { provide: AUTHDOG_CONFIG, useValue: config },\n AuthdogService,\n ];\n\n return makeEnvironmentProviders(providers);\n};\n","import { InjectionToken } from \"@angular/core\";\n\nexport interface AuthdogConfig {\n /**\n * Authdog public key (`pk_…`). Safe to expose to the browser. Used as the\n * `client_id` and to derive the identity host / environment for OIDC flows.\n */\n publicKey: string;\n\n /**\n * Path the route guard redirects to when the user is not authenticated.\n * Defaults to `/`.\n */\n loginPath?: string;\n}\n\n/** DI token carrying the SDK configuration provided via `provideAuthdog`. */\nexport const AUTHDOG_CONFIG = new InjectionToken<AuthdogConfig>(\n \"AUTHDOG_CONFIG\",\n);\n","import {\n Injectable,\n computed,\n inject,\n signal,\n type Signal,\n} from \"@angular/core\";\nimport { AUTHDOG_CONFIG } from \"./tokens\";\nimport {\n JWT_PATTERN,\n TOKEN_STORAGE_KEY,\n buildAuthorizeUrl,\n fetchUserData,\n} from \"./session\";\n\nconst isBrowser = (): boolean => typeof window !== \"undefined\";\n\n/**\n * Root-provided service that owns the client-side auth state.\n *\n * State is exposed as readonly signals so templates and `computed`s react to\n * changes automatically. The service is browser-first: every `window` /\n * `localStorage` access is guarded so it is inert under Angular Universal\n * (SSR), where it simply reports \"not loading, no token\".\n */\n@Injectable({ providedIn: \"root\" })\nexport class AuthdogService {\n private readonly config = inject(AUTHDOG_CONFIG);\n\n private readonly _token = signal<string | null>(null);\n private readonly _isLoading = signal<boolean>(true);\n private readonly _user = signal<unknown>(null);\n private readonly _error = signal<Error | null>(null);\n\n /** Current bearer token, or `null` when signed out. */\n readonly token: Signal<string | null> = this._token.asReadonly();\n /** True until the initial token-from-URL / localStorage bootstrap finishes. */\n readonly isLoading: Signal<boolean> = this._isLoading.asReadonly();\n /** The last fetched userinfo `user` payload, or `null`. */\n readonly user: Signal<unknown> = this._user.asReadonly();\n /** The last error raised by a sign-in / fetch operation, or `null`. */\n readonly error: Signal<Error | null> = this._error.asReadonly();\n\n /** True when a token is present AND a user has been loaded. */\n readonly isAuthenticated: Signal<boolean> = computed(\n () => !!this._token() && !!this._user(),\n );\n\n constructor() {\n this.bootstrap();\n }\n\n /**\n * Reads a token from the URL (`?token=`) or localStorage. A URL token is\n * validated against the JWT pattern BEFORE it is persisted, so arbitrary\n * attacker-supplied query data is never written to storage, and is stripped\n * from the address bar via `history.replaceState` regardless of validity.\n */\n private bootstrap(): void {\n if (!isBrowser()) {\n this._isLoading.set(false);\n return;\n }\n\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(\"token\");\n\n if (urlToken) {\n // Remove the token from the URL without a reload, valid or not.\n url.searchParams.delete(\"token\");\n window.history.replaceState({}, document.title, url.toString());\n\n // Only persist values that look like a JWT.\n if (JWT_PATTERN.test(urlToken)) {\n localStorage.setItem(TOKEN_STORAGE_KEY, urlToken);\n this._token.set(urlToken);\n this._isLoading.set(false);\n return;\n }\n }\n\n const existingToken = localStorage.getItem(TOKEN_STORAGE_KEY);\n if (existingToken) {\n this._token.set(existingToken);\n }\n\n this._isLoading.set(false);\n }\n\n /** Imperatively set (or clear) the in-memory + persisted token. */\n setToken(token: string | null): void {\n this._token.set(token);\n if (!isBrowser()) return;\n\n if (token) {\n localStorage.setItem(TOKEN_STORAGE_KEY, token);\n } else {\n localStorage.removeItem(TOKEN_STORAGE_KEY);\n }\n }\n\n /** Redirect the browser to the hosted sign-in page. */\n signIn(publicKey: string = this.config.publicKey, redirectUrl?: string): void {\n if (!isBrowser()) return;\n this._error.set(null);\n try {\n window.location.href = buildAuthorizeUrl(\n publicKey,\n redirectUrl || window.location.origin,\n );\n } catch (err) {\n this._error.set(err as Error);\n }\n }\n\n /** Redirect the browser to the hosted sign-up page (`prompt=signup`). */\n signUp(publicKey: string = this.config.publicKey, redirectUrl?: string): void {\n if (!isBrowser()) return;\n this._error.set(null);\n try {\n window.location.href = buildAuthorizeUrl(\n publicKey,\n redirectUrl || window.location.origin,\n { signup: true },\n );\n } catch (err) {\n this._error.set(err as Error);\n }\n }\n\n /** Clear the session locally and redirect to the logout endpoint. */\n signOut(): void {\n this.setToken(null);\n this._user.set(null);\n if (isBrowser()) {\n window.location.href = \"/logout\";\n }\n }\n\n /**\n * Fetches the current user from the identity host's userinfo endpoint and\n * stores it on the `user` signal. Returns `null` when there is no token.\n */\n async fetchUser(\n publicKey: string = this.config.publicKey,\n ): Promise<unknown> {\n const token = this._token();\n if (!token) {\n return null;\n }\n\n this._error.set(null);\n try {\n const userData = await fetchUserData(publicKey, token);\n const user = userData?.user ?? null;\n this._user.set(user);\n return user;\n } catch (err) {\n this._error.set(err as Error);\n return null;\n }\n }\n}\n","import {\n validateAndParsePublicKey,\n type PublicKeyPayload,\n} from \"@authdog/node-commons\";\n\nexport type { PublicKeyPayload };\n\n/**\n * Decodes and validates an Authdog public key. Delegates to the hardened\n * shared parser in @authdog/node-commons, which validates the payload and\n * enforces a trusted identity-host allowlist (SSRF / token-exfiltration\n * protection) rather than blindly decoding base64/JSON.\n */\nexport const getPublicKeyPayload = (publicKey: string): PublicKeyPayload => {\n return validateAndParsePublicKey(publicKey);\n};\n\n/** Lightweight, fast-fail shape check before the full decode. */\nexport const validatePublicKey = (publicKey: string): void => {\n if (!publicKey) {\n throw new Error(\"Public key is not defined\");\n }\n\n if (!publicKey.startsWith(\"pk_\")) {\n throw new Error(\"Invalid public key\");\n }\n};\n","import { getPublicKeyPayload, validatePublicKey } from \"./commons\";\n\n/** Shared localStorage key for the persisted token. */\nexport const TOKEN_STORAGE_KEY = \"token\";\n\n/** JWT shape: three base64url segments separated by dots. */\nexport const JWT_PATTERN = /^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/;\n\nexport const getTokenFromUri = (url: string): string | null => {\n return new URL(url).searchParams.get(\"token\");\n};\n\nexport interface IFetchUserData {\n user: {\n id: string;\n environmentId: string;\n externalId: string;\n userName: string;\n displayName: string;\n nickName: string;\n profileUrl: string;\n title: string;\n userType: string;\n preferredLanguage: string | null;\n locale: string | null;\n timezone: string | null;\n active: boolean;\n provider: string;\n lastLogin: string;\n createdAt: string;\n updatedAt: string;\n names: {\n id: string;\n userId: string;\n formatted: string | null;\n familyName: string;\n givenName: string;\n middleName: string | null;\n honorificPrefix: string | null;\n honorificSuffix: string | null;\n createdAt: string;\n updatedAt: string;\n };\n addresses: [];\n emails: {\n value: string;\n primary: boolean;\n type: string;\n }[];\n phoneNumbers: [];\n ims: [];\n photos: {\n value: string;\n type: string;\n }[];\n };\n meta: {\n code: number;\n message: string;\n };\n}\n\n/**\n * Fetches user data from the identity host's OIDC `userinfo` endpoint. The\n * identity host is decoded through the hardened public-key parser, which\n * enforces the trusted-host allowlist before the bearer token is ever sent.\n */\nexport const fetchUserData = async (\n publicKey: string,\n token: string,\n): Promise<IFetchUserData | null> => {\n validatePublicKey(publicKey);\n const publicKeyObj = getPublicKeyPayload(publicKey);\n\n const userData = await fetch(\n `${publicKeyObj.identityHost}/oidc/${publicKeyObj.environmentId}/userinfo`,\n {\n headers: {\n authorization: `Bearer ${token}`,\n },\n },\n );\n\n if (!userData.ok) {\n throw new Error(\"Failed to fetch user info\");\n }\n\n return (await userData.json()) as IFetchUserData;\n};\n\n/**\n * Builds the OIDC authorize URL for a sign-in or sign-up redirect. The public\n * key is decoded through the hardened parser (no raw base64/JSON) so an\n * untrusted identity host can never be used as the redirect target.\n */\nexport const buildAuthorizeUrl = (\n publicKey: string,\n redirectUri: string,\n options: { signup?: boolean } = {},\n): string => {\n validatePublicKey(publicKey);\n const publicKeyObj = getPublicKeyPayload(publicKey);\n\n const authUrl = new URL(\n `${publicKeyObj.identityHost}/oidc/${publicKeyObj.environmentId}/authorize`,\n );\n authUrl.searchParams.set(\"client_id\", publicKey);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"scope\", \"openid profile email\");\n authUrl.searchParams.set(\"redirect_uri\", redirectUri);\n\n if (options.signup) {\n authUrl.searchParams.set(\"prompt\", \"signup\");\n }\n\n return authUrl.toString();\n};\n","import { inject } from \"@angular/core\";\nimport type { HttpInterceptorFn } from \"@angular/common/http\";\nimport { AuthdogService } from \"./service\";\n\n/**\n * Functional HTTP interceptor that attaches `Authorization: Bearer <token>`\n * to outgoing requests when a session token is present. Register it with\n * `provideAuthdog()` or directly via `provideHttpClient(withInterceptors([authdogInterceptor]))`.\n *\n * Requests that already carry an `Authorization` header are left untouched.\n */\nexport const authdogInterceptor: HttpInterceptorFn = (req, next) => {\n const auth = inject(AuthdogService);\n const token = auth.token();\n\n if (!token || req.headers.has(\"Authorization\")) {\n return next(req);\n }\n\n return next(\n req.clone({\n setHeaders: { Authorization: `Bearer ${token}` },\n }),\n );\n};\n","import { inject } from \"@angular/core\";\nimport { Router, type CanActivateFn, type UrlTree } from \"@angular/router\";\nimport { AuthdogService } from \"./service\";\nimport { AUTHDOG_CONFIG } from \"./tokens\";\n\n/**\n * ⚠️ PRESENTATIONAL / UX ONLY — NOT A SECURITY BOUNDARY.\n *\n * This guard prevents an unauthenticated user from *navigating* to a route in\n * the SPA so they see a login redirect instead of a broken page. It runs\n * entirely in the browser and is therefore trivially bypassable by any client\n * (devtools, crafted requests, a patched bundle). It MUST NOT be relied on to\n * protect data or actions.\n *\n * Every protected operation MUST be independently enforced server-side: the\n * API behind the route has to validate the session on each request regardless\n * of what this guard decides.\n */\nexport const authdogGuard: CanActivateFn = (): boolean | UrlTree => {\n const auth = inject(AuthdogService);\n const router = inject(Router);\n const config = inject(AUTHDOG_CONFIG);\n\n // A token is sufficient for *navigation* — fetching the user is async and\n // we don't want to block routing on a network round-trip.\n if (auth.token()) {\n return true;\n }\n\n return router.parseUrl(config.loginPath ?? \"/\");\n};\n"],"mappings":"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","names":["makeEnvironmentProviders","InjectionToken","AUTHDOG_CONFIG","Injectable","computed","inject","signal","validateAndParsePublicKey","getPublicKeyPayload","publicKey","validatePublicKey","TOKEN_STORAGE_KEY","JWT_PATTERN","getTokenFromUri","url","fetchUserData","publicKey","token","validatePublicKey","publicKeyObj","getPublicKeyPayload","userData","buildAuthorizeUrl","redirectUri","options","authUrl","isBrowser","_AuthdogService_decorators","_init","Injectable","AuthdogService","inject","AUTHDOG_CONFIG","signal","computed","url","urlToken","JWT_PATTERN","TOKEN_STORAGE_KEY","existingToken","token","publicKey","redirectUrl","buildAuthorizeUrl","err","user","fetchUserData","__decoratorStart","__decorateElement","__runInitializers","provideAuthdog","config","makeEnvironmentProviders","AUTHDOG_CONFIG","AuthdogService","inject","authdogInterceptor","req","next","token","inject","AuthdogService","inject","Router","authdogGuard","auth","inject","AuthdogService","router","Router","config","AUTHDOG_CONFIG"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@authdog/angular",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Authdog Angular SDK",
|
|
5
|
+
"source": "src/index.ts",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/authdog-labs/web-sdk.git",
|
|
23
|
+
"directory": "packages/angular"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/authdog-labs/web-sdk/tree/main/packages/angular#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/authdog-labs/web-sdk/issues"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"format": "prettier --config .prettierrc.json --write \"**/*.{ts,md}\"",
|
|
31
|
+
"type-check": "tsc",
|
|
32
|
+
"clean": "rm -rf dist",
|
|
33
|
+
"build": "bun run clean && tsup",
|
|
34
|
+
"ship": "bun run build && bun publish --access public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@authdog/node-commons": "workspace:*"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
41
|
+
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
42
|
+
"@angular/router": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
43
|
+
"rxjs": "^7.8.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@angular/common": "^19.2.16",
|
|
47
|
+
"@angular/core": "^18.2.0",
|
|
48
|
+
"@angular/router": "^18.2.0",
|
|
49
|
+
"@types/node": "^22.10.5",
|
|
50
|
+
"dotenv": "^16.4.7",
|
|
51
|
+
"prettier": "^3.4.2",
|
|
52
|
+
"rxjs": "^7.8.1",
|
|
53
|
+
"tsup": "^8.3.5",
|
|
54
|
+
"typescript": "^5.7.2",
|
|
55
|
+
"vitest": "^2.1.8"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"registry": "https://registry.npmjs.org/",
|
|
59
|
+
"access": "public"
|
|
60
|
+
}
|
|
61
|
+
}
|