@enformik/auth 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,60 @@
1
- # Temporary Holding Version
1
+ # @enformik/auth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Enformik web sitenizde kullanıcı girişi için tarayıcı kütüphanesi: OIDC yetkilendirme kodu + PKCE, sıfır bağımlılık. Siteniz **gizli anahtar taşımaz**; giriş Enformik'in barındırdığı giriş sayfasında yapılır, siteniz yalnızca bu uygulamaya özel kısa ömürlü bir oturum belirteci alır.
4
+
5
+ Tam kılavuz (kurulum, web kullanıcıları, erişim kuralları): <https://help.enformik.com/rehber/web-sitesi-girisi/>
6
+
7
+ ## Önkoşul
8
+
9
+ Konsolda web sitenizin **Geliştirici sitesi → Site girişi** sekmesinden girişi açıp siteyi yeniden dağıtın. Her dağıtımda `/_enformik/config.json` içine giriş bilgileri (`auth` bloğu) yazılır; kütüphane yapılandırmayı buradan okur.
10
+
11
+ ## Kullanım
12
+
13
+ ```bash
14
+ npm install @enformik/auth
15
+ ```
16
+
17
+ ```js
18
+ import { createAuth } from '@enformik/auth';
19
+
20
+ const auth = await createAuth();
21
+
22
+ // Sayfa yüklenirken giriş dönüşünü tamamlayın (dönüş yoksa hiçbir şey yapmaz).
23
+ const done = await auth.handleRedirect();
24
+ if (done) history.replaceState(null, '', done.returnTo);
25
+
26
+ if (!auth.isAuthenticated()) {
27
+ await auth.login(); // giriş sayfasına yönlendirir
28
+ } else {
29
+ // Bearer ve çalışma alanı başlığı otomatik eklenir; süre dolarsa belirteç yenilenir.
30
+ const res = await auth.fetch('/data/musteri?limit=20');
31
+ console.log(auth.getUser(), await res.json());
32
+ }
33
+
34
+ // await auth.logout();
35
+ ```
36
+
37
+ ## API
38
+
39
+ | Üye | Açıklama |
40
+ |---|---|
41
+ | `createAuth(options?)` | Yapılandırmayı okuyup istemciyi kurar; site girişi açık değilse `not_configured` hatası verir. |
42
+ | `config` | Okunan site yapılandırması (`appUrl`, `apiUrl`, `workspaceId`, `auth`…). |
43
+ | `isAuthenticated()` | Geçerli ya da yenilenebilir oturum var mı. |
44
+ | `login(returnTo?)` | Giriş sayfasına yönlendirir; `returnTo` giriş sonrası dönülecek yoldur. |
45
+ | `handleRedirect()` | Girişten dönüşü tamamlar; `{ returnTo }` ya da `null` döner. |
46
+ | `getAccessToken()` | Geçerli access token'ı döner, gerekirse yeniler (eşzamanlı çağrılar tek yenilemede birleşir). |
47
+ | `fetch(path, init?)` | Uygulamanın API'sini oturumla çağırır; 401'de bir kez yenileyip tekrarlar. |
48
+ | `getUser()` | ID token'dan `{ sub, email?, name? }` — yalnızca arayüzde göstermek içindir. |
49
+ | `logout()` | Oturumu siler ve giriş sayfasından çıkış yaptırır. |
50
+
51
+ Seçenekler (`AuthOptions`): `config` (nesne ya da adres; varsayılan `/_enformik/config.json`), `redirectPath` (varsayılan `/callback`), `postLogoutPath` (varsayılan `/`), `storage` (`'session'` varsayılan, `'local'` ya da kendi `getItem/setItem/removeItem` nesneniz).
52
+
53
+ `redirectPath` sitenizin kayıtlı dönüş adreslerinden biri olmalıdır: `/`, `/callback`, `/auth/callback`, `/login/callback`.
54
+
55
+ Hatalar `EnformikAuthError` olarak atılır; `code` alanı: `not_configured`, `state_mismatch`, `no_pending_login`, `login_rejected`, `token_exchange_failed`, `not_authenticated`.
56
+
57
+ ## Güvenlik notları
58
+
59
+ - Belirteçler varsayılan olarak `sessionStorage`'da tutulur (sekme kapanınca silinir); `'local'` seçerseniz sitenizdeki herhangi bir betik okuyabilir — üçüncü taraf betik yüklüyorsanız önermiyoruz.
60
+ - Oturum belirteci yalnızca bu uygulamanın API'sinde geçerlidir; kullanıcının ne görebileceğini sitenizin değil, uygulamanızdaki **Kayıt erişim kuralları** belirler.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { type AuthConfig, type AuthOptions, type AuthUser, type SiteConfig } from './types.js';
2
+ export interface Auth {
3
+ /** Okunan site yapılandırması (`auth` bloğu doludur; yoksa `createAuth` zaten `not_configured` verir). */
4
+ readonly config: SiteConfig & {
5
+ auth: AuthConfig;
6
+ };
7
+ /** Oturum var mı (geçerli access token ya da yenilenebilir oturum). */
8
+ isAuthenticated(): boolean;
9
+ /** Giriş sayfasına yönlendirir. `returnTo`: giriş sonrası dönülecek yol (varsayılan: şu anki yol). */
10
+ login(returnTo?: string): Promise<void>;
11
+ /**
12
+ * Dönüş adresinde (`?code=&state=`) girişi tamamlar. Her sayfa yüklemesinde çağırmak güvenlidir: dönüş yoksa `null` döner.
13
+ * Başarıda `{ returnTo }` döner (sayfayı oraya yönlendirmek istemci koduna bırakılır).
14
+ */
15
+ handleRedirect(): Promise<{
16
+ returnTo: string;
17
+ } | null>;
18
+ /** Geçerli access token'ı döner (gerekirse yeniler); oturum yoksa `null`. */
19
+ getAccessToken(): Promise<string | null>;
20
+ /** Uygulamanın API'sini oturum belirteciyle çağırır. `path` apiUrl'ye göre göreli ya da tam adres olabilir. */
21
+ fetch(path: string, init?: RequestInit): Promise<Response>;
22
+ /** ID token'dan kullanıcı bilgisi (yalnızca arayüz için). */
23
+ getUser(): AuthUser | null;
24
+ /** Oturumu siler ve sağlayıcıdan çıkış yaptırır. */
25
+ logout(): Promise<void>;
26
+ }
27
+ /** Siteyi Enformik girişine bağlar. Yapılandırmada `auth` yoksa `not_configured` hatası verir. */
28
+ export declare function createAuth(opts?: AuthOptions): Promise<Auth>;
package/dist/auth.js ADDED
@@ -0,0 +1,199 @@
1
+ import { challengeFor, randomString } from './pkce.js';
2
+ import { EnformikAuthError } from './types.js';
3
+ const PENDING_KEY = 'enformik.auth.pending';
4
+ const TOKENS_KEY = 'enformik.auth.tokens';
5
+ const LEEWAY_MS = 30_000; // süresi dolmadan 30 sn önce yenile
6
+ function decodeJwtPayload(token) {
7
+ try {
8
+ const part = token.split('.')[1];
9
+ if (!part)
10
+ return null;
11
+ const b64 = part.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (part.length % 4)) % 4);
12
+ return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(b64), c => c.charCodeAt(0))));
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /** Siteyi Enformik girişine bağlar. Yapılandırmada `auth` yoksa `not_configured` hatası verir. */
19
+ export async function createAuth(opts = {}) {
20
+ const f = opts.fetch ?? globalThis.fetch.bind(globalThis);
21
+ const loc = opts.location ?? globalThis.location;
22
+ const hist = opts.history ?? globalThis.history;
23
+ const cry = opts.crypto ?? globalThis.crypto;
24
+ const now = opts.now ?? Date.now;
25
+ const redirectPath = opts.redirectPath ?? '/callback';
26
+ const postLogoutPath = opts.postLogoutPath ?? '/';
27
+ const store = typeof opts.storage === 'object' ? opts.storage : opts.storage === 'local' ? globalThis.localStorage : globalThis.sessionStorage;
28
+ let cfg;
29
+ if (opts.config && typeof opts.config === 'object') {
30
+ cfg = opts.config;
31
+ }
32
+ else {
33
+ const url = typeof opts.config === 'string' ? opts.config : '/_enformik/config.json';
34
+ const res = await f(url, { headers: { Accept: 'application/json' } });
35
+ if (!res.ok)
36
+ throw new EnformikAuthError('not_configured', `Yapılandırma okunamadı (${url}: HTTP ${res.status}).`);
37
+ cfg = (await res.json());
38
+ }
39
+ const authCfg = cfg.auth;
40
+ if (!authCfg?.authority || !authCfg.clientId || !authCfg.gatewayUrl) {
41
+ throw new EnformikAuthError('not_configured', 'Bu site için giriş açık değil: yapılandırmada "auth" bloğu yok (konsolda Site girişi açıp siteyi yeniden dağıtın).');
42
+ }
43
+ const a = authCfg;
44
+ const redirectUri = () => loc.origin + redirectPath;
45
+ const tokenUrl = a.authority.replace(/\/$/, '') + '/oauth/v2/token';
46
+ const read = (key) => {
47
+ try {
48
+ const v = store.getItem(key);
49
+ return v ? JSON.parse(v) : null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ };
55
+ const write = (key, v) => store.setItem(key, JSON.stringify(v));
56
+ const saveTokens = (t, prev) => {
57
+ const expiresIn = typeof t.expires_in === 'number' ? t.expires_in : 3600;
58
+ const tokens = {
59
+ accessToken: String(t.access_token),
60
+ refreshToken: t.refresh_token ?? prev?.refreshToken,
61
+ idToken: t.id_token ?? prev?.idToken,
62
+ expiresAt: now() + expiresIn * 1000,
63
+ };
64
+ write(TOKENS_KEY, tokens);
65
+ return tokens;
66
+ };
67
+ const tokenRequest = async (params) => {
68
+ const res = await f(tokenUrl, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
71
+ body: new URLSearchParams({ client_id: a.clientId, ...params }).toString(),
72
+ });
73
+ const body = (await res.json().catch(() => ({})));
74
+ if (!res.ok || typeof body.access_token !== 'string') {
75
+ throw new EnformikAuthError('token_exchange_failed', String(body.error_description ?? body.error ?? `HTTP ${res.status}`));
76
+ }
77
+ return body;
78
+ };
79
+ // Aynı anda birden çok yenileme isteği tek isteğe indirgenir (yenileme belirteci döndürülüyorsa ikincisi başarısız olurdu).
80
+ let refreshing = null;
81
+ const refresh = () => {
82
+ if (refreshing)
83
+ return refreshing;
84
+ const cur = read(TOKENS_KEY);
85
+ if (!cur?.refreshToken) {
86
+ store.removeItem(TOKENS_KEY);
87
+ return Promise.resolve(null);
88
+ }
89
+ refreshing = tokenRequest({ grant_type: 'refresh_token', refresh_token: cur.refreshToken })
90
+ .then(body => saveTokens(body, cur))
91
+ .catch(() => {
92
+ store.removeItem(TOKENS_KEY);
93
+ return null;
94
+ })
95
+ .finally(() => {
96
+ refreshing = null;
97
+ });
98
+ return refreshing;
99
+ };
100
+ const getAccessToken = async () => {
101
+ const t = read(TOKENS_KEY);
102
+ if (!t)
103
+ return null;
104
+ if (t.expiresAt - now() > LEEWAY_MS)
105
+ return t.accessToken;
106
+ return (await refresh())?.accessToken ?? null;
107
+ };
108
+ return {
109
+ config: { ...cfg, auth: a },
110
+ isAuthenticated() {
111
+ const t = read(TOKENS_KEY);
112
+ return !!t && (t.expiresAt > now() || !!t.refreshToken);
113
+ },
114
+ async login(returnTo) {
115
+ const verifier = randomString(32, cry);
116
+ const state = randomString(16, cry);
117
+ write(PENDING_KEY, { state, verifier, returnTo: returnTo ?? loc.href.slice(loc.origin.length) });
118
+ const q = new URLSearchParams({
119
+ client_id: a.clientId,
120
+ redirect_uri: redirectUri(),
121
+ code_challenge: await challengeFor(verifier, cry),
122
+ code_challenge_method: 'S256',
123
+ state,
124
+ });
125
+ loc.assign(a.gatewayUrl.replace(/\/$/, '') + '/login?' + q.toString());
126
+ },
127
+ async handleRedirect() {
128
+ const q = new URLSearchParams(loc.search);
129
+ const err = q.get('error');
130
+ const code = q.get('code');
131
+ if (!err && !code)
132
+ return null;
133
+ const pending = read(PENDING_KEY);
134
+ store.removeItem(PENDING_KEY); // tek kullanımlık
135
+ const clean = () => hist.replaceState(null, '', loc.pathname);
136
+ if (err) {
137
+ clean();
138
+ throw new EnformikAuthError('login_rejected', q.get('error_description') ?? err);
139
+ }
140
+ if (!pending) {
141
+ clean();
142
+ throw new EnformikAuthError('no_pending_login', 'Bekleyen bir giriş yok; girişi yeniden başlatın.');
143
+ }
144
+ if (q.get('state') !== pending.state) {
145
+ clean();
146
+ throw new EnformikAuthError('state_mismatch', 'Giriş yanıtı bu oturumla eşleşmiyor.');
147
+ }
148
+ try {
149
+ const body = await tokenRequest({ grant_type: 'authorization_code', code: code, redirect_uri: redirectUri(), code_verifier: pending.verifier });
150
+ saveTokens(body);
151
+ }
152
+ finally {
153
+ clean();
154
+ }
155
+ return { returnTo: pending.returnTo };
156
+ },
157
+ getAccessToken,
158
+ async fetch(path, init = {}) {
159
+ const base = cfg.apiUrl.replace(/\/$/, '');
160
+ const url = /^https?:\/\//i.test(path) ? path : base + (path.startsWith('/') ? path : '/' + path);
161
+ const call = async (token) => {
162
+ const h = new Headers(init.headers);
163
+ h.set('Authorization', 'Bearer ' + token);
164
+ if (cfg.workspaceId && !h.has('X-Tenant-ID'))
165
+ h.set('X-Tenant-ID', cfg.workspaceId);
166
+ return f(url, { ...init, headers: h });
167
+ };
168
+ let token = await getAccessToken();
169
+ if (!token)
170
+ throw new EnformikAuthError('not_authenticated', 'Oturum yok; önce giriş yapın.');
171
+ let res = await call(token);
172
+ if (res.status === 401) {
173
+ // Token sunucuda reddedildi (iptal/erken bitiş): bir kez yenile ve tekrar dene.
174
+ const t = await refresh();
175
+ if (!t)
176
+ throw new EnformikAuthError('not_authenticated', 'Oturum sona erdi; yeniden giriş yapın.');
177
+ token = t.accessToken;
178
+ res = await call(token);
179
+ }
180
+ return res;
181
+ },
182
+ getUser() {
183
+ const id = read(TOKENS_KEY)?.idToken;
184
+ const c = id ? decodeJwtPayload(id) : null;
185
+ if (!c || typeof c.sub !== 'string')
186
+ return null;
187
+ return { sub: c.sub, email: typeof c.email === 'string' ? c.email : undefined, name: typeof c.name === 'string' ? c.name : undefined };
188
+ },
189
+ async logout() {
190
+ const idToken = read(TOKENS_KEY)?.idToken;
191
+ store.removeItem(TOKENS_KEY);
192
+ store.removeItem(PENDING_KEY);
193
+ const q = new URLSearchParams({ client_id: a.clientId, post_logout_redirect_uri: loc.origin + postLogoutPath });
194
+ if (idToken)
195
+ q.set('id_token_hint', idToken);
196
+ loc.assign(a.authority.replace(/\/$/, '') + '/oidc/v1/end_session?' + q.toString());
197
+ },
198
+ };
199
+ }
@@ -0,0 +1,4 @@
1
+ export { createAuth } from './auth.js';
2
+ export type { Auth } from './auth.js';
3
+ export { EnformikAuthError } from './types.js';
4
+ export type { AuthConfig, AuthErrorCode, AuthOptions, AuthUser, KeyValueStorage, SiteConfig } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createAuth } from './auth.js';
2
+ export { EnformikAuthError } from './types.js';
package/dist/pkce.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Bayt dizisini Base64URL (dolgusuz) metne çevirir. */
2
+ export declare function base64url(bytes: Uint8Array): string;
3
+ /** Rastgele, URL'de güvenli bir metin üretir (n bayt entropi). */
4
+ export declare function randomString(n: number, c?: Crypto): string;
5
+ /** code_verifier'dan S256 code_challenge üretir. */
6
+ export declare function challengeFor(verifier: string, c?: Crypto): Promise<string>;
package/dist/pkce.js ADDED
@@ -0,0 +1,19 @@
1
+ // PKCE (RFC 7636) yardımcıları: Web Crypto, sıfır bağımlılık.
2
+ /** Bayt dizisini Base64URL (dolgusuz) metne çevirir. */
3
+ export function base64url(bytes) {
4
+ let bin = '';
5
+ for (const b of bytes)
6
+ bin += String.fromCharCode(b);
7
+ return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
8
+ }
9
+ /** Rastgele, URL'de güvenli bir metin üretir (n bayt entropi). */
10
+ export function randomString(n, c = globalThis.crypto) {
11
+ const b = new Uint8Array(n);
12
+ c.getRandomValues(b);
13
+ return base64url(b);
14
+ }
15
+ /** code_verifier'dan S256 code_challenge üretir. */
16
+ export async function challengeFor(verifier, c = globalThis.crypto) {
17
+ const digest = await c.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
18
+ return base64url(new Uint8Array(digest));
19
+ }
@@ -0,0 +1,53 @@
1
+ /** `/_enformik/config.json` içindeki `auth` bloğu (site girişi açıkken yazılır). */
2
+ export interface AuthConfig {
3
+ /** OIDC yayıncısı; token uç noktası `${authority}/oauth/v2/token`. */
4
+ authority: string;
5
+ /** Sitenin istemci kimliği (sırsız). */
6
+ clientId: string;
7
+ /** Barındırılan giriş sayfası; `${gatewayUrl}/login`. */
8
+ gatewayUrl: string;
9
+ /** Uygulamanın API projesi (access token'ın hedefi). */
10
+ audience: string;
11
+ }
12
+ /** `/_enformik/config.json` içeriği. */
13
+ export interface SiteConfig {
14
+ appUrl: string;
15
+ apiUrl: string;
16
+ workspaceId?: string;
17
+ appCode?: string;
18
+ siteUrl?: string;
19
+ auth?: AuthConfig;
20
+ }
21
+ /** Oturumdaki kullanıcının kimliği (ID token'dan; yalnızca arayüzde göstermek içindir, API'ye gönderilmez). */
22
+ export interface AuthUser {
23
+ sub: string;
24
+ email?: string;
25
+ name?: string;
26
+ }
27
+ export type AuthErrorCode = 'not_configured' | 'state_mismatch' | 'no_pending_login' | 'login_rejected' | 'token_exchange_failed' | 'not_authenticated';
28
+ export declare class EnformikAuthError extends Error {
29
+ readonly code: AuthErrorCode;
30
+ constructor(code: AuthErrorCode, message: string);
31
+ }
32
+ /** Depolama soyutlaması (sessionStorage/localStorage ile uyumlu alt küme). */
33
+ export interface KeyValueStorage {
34
+ getItem(key: string): string | null;
35
+ setItem(key: string, value: string): void;
36
+ removeItem(key: string): void;
37
+ }
38
+ export interface AuthOptions {
39
+ /** Yapılandırma: nesne ya da adres. Varsayılan: `/_enformik/config.json`. */
40
+ config?: SiteConfig | string;
41
+ /** Giriş dönüş yolu; sitenin kayıtlı adreslerinden biri olmalı (`/`, `/callback`, `/auth/callback`, `/login/callback`). Varsayılan `/callback`. */
42
+ redirectPath?: string;
43
+ /** Çıkış sonrası dönüş yolu (`/` ya da `/login`). Varsayılan `/`. */
44
+ postLogoutPath?: string;
45
+ /** Belirteçlerin saklandığı yer: 'session' (varsayılan, sekme kapanınca silinir) ya da 'local'. */
46
+ storage?: 'session' | 'local' | KeyValueStorage;
47
+ /** Test/özel ortam için enjekte edilebilir bağımlılıklar. */
48
+ fetch?: typeof fetch;
49
+ location?: Pick<Location, 'origin' | 'href' | 'search' | 'pathname' | 'assign'>;
50
+ history?: Pick<History, 'replaceState'>;
51
+ crypto?: Crypto;
52
+ now?: () => number;
53
+ }
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ export class EnformikAuthError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.name = 'EnformikAuthError';
6
+ this.code = code;
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@enformik/auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Enformik web sitesi girişi: tarayıcı için OIDC yetkilendirme kodu + PKCE istemcisi (sıfır bağımlılık).",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "sideEffects": false,
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "test": "vitest run",
27
+ "prepublishOnly": "npm run build && npm test && npm run smoke",
28
+ "smoke": "node --input-type=module -e \"const m = await import('./dist/index.js'); if (typeof m.createAuth !== 'function') throw new Error('createAuth yok')\""
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.0",
32
+ "typescript": "^5.7.0",
33
+ "vitest": "^4.1.8"
34
+ },
35
+ "keywords": [
36
+ "enformik",
37
+ "auth",
38
+ "oidc",
39
+ "pkce",
40
+ "browser"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/eyote-com/eyote.git",
45
+ "directory": "apps/enformik-auth"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/eyote-com/eyote/issues"
49
+ }
50
+ }