@sentroy-co/client-sdk 2.13.7 → 2.13.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Sentroy Auth-as-a-Service — browser/server SDK entry.
3
+ *
4
+ * import { SentroyAuth } from "@sentroy-co/client-sdk/auth"
5
+ * const auth = new SentroyAuth({ projectSlug: "my-app" })
6
+ *
7
+ * For server admin operations (verifyIdToken, etc):
8
+ * import { SentroyAuthAdmin } from "@sentroy-co/client-sdk/auth/admin"
9
+ *
10
+ * For React integration:
11
+ * import { SentroyAuthProvider, useAuth } from "@sentroy-co/client-sdk/auth/react"
12
+ */
13
+ export { SentroyAuth } from "./client";
14
+ export type { SentroyAuthOptions, AuthStateChangeListener, AuthStorageAdapter, } from "./client";
15
+ export { SentroyAuthError, type SentroyAuthUser, type SignupResponse, type LoginResponse, type AuthTokensResponse, } from "./types";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACtC,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,GACxB,MAAM,SAAS,CAAA"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /**
3
+ * Sentroy Auth-as-a-Service — browser/server SDK entry.
4
+ *
5
+ * import { SentroyAuth } from "@sentroy-co/client-sdk/auth"
6
+ * const auth = new SentroyAuth({ projectSlug: "my-app" })
7
+ *
8
+ * For server admin operations (verifyIdToken, etc):
9
+ * import { SentroyAuthAdmin } from "@sentroy-co/client-sdk/auth/admin"
10
+ *
11
+ * For React integration:
12
+ * import { SentroyAuthProvider, useAuth } from "@sentroy-co/client-sdk/auth/react"
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SentroyAuthError = exports.SentroyAuth = void 0;
16
+ var client_1 = require("./client");
17
+ Object.defineProperty(exports, "SentroyAuth", { enumerable: true, get: function () { return client_1.SentroyAuth; } });
18
+ var types_1 = require("./types");
19
+ Object.defineProperty(exports, "SentroyAuthError", { enumerable: true, get: function () { return types_1.SentroyAuthError; } });
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAEH,mCAAsC;AAA7B,qGAAA,WAAW,OAAA;AAMpB,iCAMgB;AALd,yGAAA,gBAAgB,OAAA"}
@@ -0,0 +1,41 @@
1
+ import { type ReactNode } from "react";
2
+ import { SentroyAuth, type SentroyAuthOptions } from "../client";
3
+ import type { SentroyAuthUser } from "../types";
4
+ /**
5
+ * Sentroy Auth React integration.
6
+ *
7
+ * <SentroyAuthProvider projectSlug="my-app">
8
+ * <App />
9
+ * </SentroyAuthProvider>
10
+ *
11
+ * const { user, loading, signIn, signOut } = useAuth()
12
+ *
13
+ * Provider içeride tek bir `SentroyAuth` instance tutar (mount/unmount
14
+ * arasında stable), `onAuthStateChanged` ile React state'i senkron tutar.
15
+ * `loading` ilk render → restore tamam mı henüz değil ayrımı için.
16
+ */
17
+ interface AuthContextValue {
18
+ auth: SentroyAuth;
19
+ user: SentroyAuthUser | null;
20
+ /** True iken provider ilk state'i restore etmiş değil — UI'da
21
+ * "spinner" göster, "redirect to /login" tetikleme. */
22
+ loading: boolean;
23
+ /** Convenience proxies — caller `auth.signIn(...)` yerine doğrudan
24
+ * `signIn(...)` kullanabilir. */
25
+ signIn: SentroyAuth["signIn"];
26
+ signUp: SentroyAuth["signUp"];
27
+ signOut: SentroyAuth["signOut"];
28
+ sendPasswordReset: SentroyAuth["sendPasswordReset"];
29
+ verifyEmail: SentroyAuth["verifyEmail"];
30
+ }
31
+ export declare function SentroyAuthProvider({ children, ...opts }: SentroyAuthOptions & {
32
+ children: ReactNode;
33
+ }): import("react/jsx-runtime").JSX.Element;
34
+ export declare function useAuth(): AuthContextValue;
35
+ /**
36
+ * Convenience: yalnızca current user istenirse. `loading` durumunda null
37
+ * dönerken bekleyebilirsin.
38
+ */
39
+ export declare function useUser(): SentroyAuthUser | null;
40
+ export {};
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/react/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE/C;;;;;;;;;;;;GAYG;AAEH,UAAU,gBAAgB;IACxB,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,EAAE,eAAe,GAAG,IAAI,CAAA;IAC5B;4DACwD;IACxD,OAAO,EAAE,OAAO,CAAA;IAChB;sCACkC;IAClC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IAC7B,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IAC7B,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;IAC/B,iBAAiB,EAAE,WAAW,CAAC,mBAAmB,CAAC,CAAA;IACnD,WAAW,EAAE,WAAW,CAAC,aAAa,CAAC,CAAA;CACxC;AAID,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,kBAAkB,GAAG;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAkC9C;AAED,wBAAgB,OAAO,IAAI,gBAAgB,CAQ1C;AAED;;;GAGG;AACH,wBAAgB,OAAO,IAAI,eAAe,GAAG,IAAI,CAEhD"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SentroyAuthProvider = SentroyAuthProvider;
5
+ exports.useAuth = useAuth;
6
+ exports.useUser = useUser;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const client_1 = require("../client");
10
+ const AuthContext = (0, react_1.createContext)(null);
11
+ function SentroyAuthProvider({ children, ...opts }) {
12
+ // Single instance — opts deep-compare'a girersek dependency drift'i
13
+ // restart'a yol açar. Caller `projectSlug` değiştirmemeli runtime'da.
14
+ const auth = (0, react_1.useMemo)(() => new client_1.SentroyAuth(opts),
15
+ // eslint-disable-next-line react-hooks/exhaustive-deps
16
+ [opts.projectSlug, opts.authBaseUrl, opts.apiKey]);
17
+ const [user, setUser] = (0, react_1.useState)(auth.user);
18
+ const [loading, setLoading] = (0, react_1.useState)(true);
19
+ (0, react_1.useEffect)(() => {
20
+ const unsubscribe = auth.onAuthStateChanged((u) => {
21
+ setUser(u);
22
+ setLoading(false);
23
+ });
24
+ return unsubscribe;
25
+ }, [auth]);
26
+ const value = (0, react_1.useMemo)(() => ({
27
+ auth,
28
+ user,
29
+ loading,
30
+ signIn: (i) => auth.signIn(i),
31
+ signUp: (i) => auth.signUp(i),
32
+ signOut: () => auth.signOut(),
33
+ sendPasswordReset: (e) => auth.sendPasswordReset(e),
34
+ verifyEmail: (t) => auth.verifyEmail(t),
35
+ }), [auth, user, loading]);
36
+ return (0, jsx_runtime_1.jsx)(AuthContext.Provider, { value: value, children: children });
37
+ }
38
+ function useAuth() {
39
+ const ctx = (0, react_1.useContext)(AuthContext);
40
+ if (!ctx) {
41
+ throw new Error("useAuth must be used inside <SentroyAuthProvider> — wrap your app root.");
42
+ }
43
+ return ctx;
44
+ }
45
+ /**
46
+ * Convenience: yalnızca current user istenirse. `loading` durumunda null
47
+ * dönerken bekleyebilirsin.
48
+ */
49
+ function useUser() {
50
+ return useAuth().user;
51
+ }
52
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/auth/react/index.tsx"],"names":[],"mappings":";AAAA,YAAY,CAAA;;AA4CZ,kDAqCC;AAED,0BAQC;AAMD,0BAEC;;AAjGD,iCAOc;AACd,sCAAgE;AAgChE,MAAM,WAAW,GAAG,IAAA,qBAAa,EAA0B,IAAI,CAAC,CAAA;AAEhE,SAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,GAAG,IAAI,EACsC;IAC7C,oEAAoE;IACpE,sEAAsE;IACtE,MAAM,IAAI,GAAG,IAAA,eAAO,EAClB,GAAG,EAAE,CAAC,IAAI,oBAAW,CAAC,IAAI,CAAC;IAC3B,uDAAuD;IACvD,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAClD,CAAA;IACD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,IAAA,gBAAQ,EAAyB,IAAI,CAAC,IAAI,CAAC,CAAA;IACnE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,IAAA,gBAAQ,EAAC,IAAI,CAAC,CAAA;IAE5C,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,MAAM,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,EAAE;YAChD,OAAO,CAAC,CAAC,CAAC,CAAA;YACV,UAAU,CAAC,KAAK,CAAC,CAAA;QACnB,CAAC,CAAC,CAAA;QACF,OAAO,WAAW,CAAA;IACpB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAEV,MAAM,KAAK,GAAG,IAAA,eAAO,EACnB,GAAG,EAAE,CAAC,CAAC;QACL,IAAI;QACJ,IAAI;QACJ,OAAO;QACP,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7B,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7B,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE;QAC7B,iBAAiB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC;QACnD,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;KACxC,CAAC,EACF,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CACtB,CAAA;IAED,OAAO,uBAAC,WAAW,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAwB,CAAA;AAC9E,CAAC;AAED,SAAgB,OAAO;IACrB,MAAM,GAAG,GAAG,IAAA,kBAAU,EAAC,WAAW,CAAC,CAAA;IACnC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,yEAAyE,CAC1E,CAAA;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;GAGG;AACH,SAAgB,OAAO;IACrB,OAAO,OAAO,EAAE,CAAC,IAAI,CAAA;AACvB,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Sentroy Auth-as-a-Service — SDK types.
3
+ *
4
+ * Public types are kept narrow on purpose: SDK shapes evolve with backend;
5
+ * caller code should depend on these names, not on hand-coded interfaces.
6
+ */
7
+ export interface SentroyAuthUser {
8
+ id: string;
9
+ authProjectId: string;
10
+ email: string;
11
+ emailVerified: boolean;
12
+ displayName: string | null;
13
+ image: string | null;
14
+ metadata: Record<string, unknown>;
15
+ lastLoginAt: string | null;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ }
19
+ export interface AuthTokensResponse {
20
+ accessToken: string;
21
+ refreshToken: string;
22
+ expiresIn: number;
23
+ tokenType: "Bearer";
24
+ }
25
+ export interface SignupResponse {
26
+ user: SentroyAuthUser;
27
+ /** Email verification gerekiyorsa undefined; aksi halde set. */
28
+ accessToken?: string;
29
+ refreshToken?: string;
30
+ expiresIn?: number;
31
+ tokenType?: "Bearer";
32
+ emailVerificationRequired?: boolean;
33
+ }
34
+ export interface LoginResponse {
35
+ user: SentroyAuthUser;
36
+ accessToken: string;
37
+ refreshToken: string;
38
+ expiresIn: number;
39
+ tokenType: "Bearer";
40
+ }
41
+ export interface AuthApiError {
42
+ error: string;
43
+ error_description: string;
44
+ }
45
+ export declare class SentroyAuthError extends Error {
46
+ readonly code: string;
47
+ readonly status: number;
48
+ constructor(code: string, message: string, status: number);
49
+ }
50
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,OAAO,CAAA;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,QAAQ,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,eAAe,CAAA;IACrB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,QAAQ,CAAA;IACpB,yBAAyB,CAAC,EAAE,OAAO,CAAA;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,eAAe,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,QAAQ,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;gBACX,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAM1D"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * Sentroy Auth-as-a-Service — SDK types.
4
+ *
5
+ * Public types are kept narrow on purpose: SDK shapes evolve with backend;
6
+ * caller code should depend on these names, not on hand-coded interfaces.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.SentroyAuthError = void 0;
10
+ class SentroyAuthError extends Error {
11
+ code;
12
+ status;
13
+ constructor(code, message, status) {
14
+ super(message);
15
+ this.name = "SentroyAuthError";
16
+ this.code = code;
17
+ this.status = status;
18
+ }
19
+ }
20
+ exports.SentroyAuthError = SentroyAuthError;
21
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AA6CH,MAAa,gBAAiB,SAAQ,KAAK;IAChC,IAAI,CAAQ;IACZ,MAAM,CAAQ;IACvB,YAAY,IAAY,EAAE,OAAe,EAAE,MAAc;QACvD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;CACF;AATD,4CASC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentroy-co/client-sdk",
3
- "version": "2.13.7",
3
+ "version": "2.13.8",
4
4
  "description": "TypeScript SDK + CLI for the Sentroy platform — mail, storage, env vault + React components.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,6 +38,21 @@
38
38
  "types": "./dist/vault/react.d.ts",
39
39
  "import": "./dist/vault/react.js",
40
40
  "require": "./dist/vault/react.js"
41
+ },
42
+ "./auth": {
43
+ "types": "./dist/auth/index.d.ts",
44
+ "import": "./dist/auth/index.js",
45
+ "require": "./dist/auth/index.js"
46
+ },
47
+ "./auth/admin": {
48
+ "types": "./dist/auth/admin/index.d.ts",
49
+ "import": "./dist/auth/admin/index.js",
50
+ "require": "./dist/auth/admin/index.js"
51
+ },
52
+ "./auth/react": {
53
+ "types": "./dist/auth/react/index.d.ts",
54
+ "import": "./dist/auth/react/index.js",
55
+ "require": "./dist/auth/react/index.js"
41
56
  }
42
57
  },
43
58
  "files": [
@@ -0,0 +1,191 @@
1
+ import { AuthHttp } from "../http"
2
+ import type { SentroyAuthUser } from "../types"
3
+
4
+ /**
5
+ * Server-side Sentroy Auth admin SDK. **Node only — apiKey browser'a
6
+ * koymayın**; bu sınıf Project'in master `aps_` token'ını taşır ve
7
+ * Sentroy üzerindeki user pool'a yetki vermez.
8
+ *
9
+ * Tipik kullanım: backend, kendi `/api/auth/...` proxy'sinde RP-spesifik
10
+ * authorization yapar, sonra `admin.users.get(...)` ile Sentroy'dan
11
+ * end-user'ı çeker. JWT verify de bu SDK üzerinden — tüm akış stateless.
12
+ */
13
+
14
+ export interface SentroyAuthAdminOptions {
15
+ authBaseUrl?: string
16
+ projectSlug: string
17
+ apiKey: string
18
+ }
19
+
20
+ export class SentroyAuthAdmin {
21
+ private readonly http: AuthHttp
22
+ private cachedJwks: { keys: Record<string, unknown>[] } | null = null
23
+
24
+ constructor(opts: SentroyAuthAdminOptions) {
25
+ this.http = new AuthHttp(opts)
26
+ }
27
+
28
+ // ─── User pool admin ──────────────────────────────────────────────────
29
+
30
+ users = {
31
+ list: (opts: {
32
+ limit?: number
33
+ skip?: number
34
+ emailVerified?: boolean
35
+ } = {}): Promise<{
36
+ items: SentroyAuthUser[]
37
+ pagination: { total: number; limit: number; skip: number }
38
+ }> => {
39
+ throw new Error(
40
+ "admin.users.list requires session-authenticated admin API; use dashboard /api/companies/[slug]/auth-projects/[id]/users instead. (v2 admin SDK will proxy this with stk_ tokens.)",
41
+ )
42
+ // NOTE Phase 5+: SDK admin endpoint'leri public path'lere taşınmadı;
43
+ // şu an `/api/companies/...` cookie-auth ile. v2'de `/api/v1/admin/...`
44
+ // RP token'ı ile authenticate eden ayrı public admin layer eklenir.
45
+ },
46
+ }
47
+
48
+ // ─── ID token verification ─────────────────────────────────────────────
49
+
50
+ /**
51
+ * Local verify — JWKS cache'lenir (5dk TTL), JWT signature kontrolü
52
+ * RS256 ile RP backend'inde stateless yapılır. `iss`/`aud` claim
53
+ * eşleşmesi de kontrol edilir.
54
+ */
55
+ async verifyIdToken(token: string): Promise<{
56
+ sub: string
57
+ email?: string
58
+ email_verified?: boolean
59
+ name?: string
60
+ picture?: string
61
+ iss: string
62
+ aud: string
63
+ iat: number
64
+ exp: number
65
+ }> {
66
+ const parts = token.split(".")
67
+ if (parts.length !== 3) {
68
+ throw new Error("Malformed JWT — expected three segments.")
69
+ }
70
+ const [headerB64, payloadB64, sigB64] = parts
71
+ const header = JSON.parse(decodeBase64Url(headerB64)) as {
72
+ alg?: string
73
+ kid?: string
74
+ }
75
+ if (header.alg !== "RS256") {
76
+ throw new Error("Only RS256 supported.")
77
+ }
78
+ const claims = JSON.parse(decodeBase64Url(payloadB64)) as {
79
+ exp?: number
80
+ iss?: string
81
+ aud?: string
82
+ }
83
+ if (typeof claims.exp !== "number" || claims.exp * 1000 < Date.now()) {
84
+ throw new Error("Token expired.")
85
+ }
86
+ // iss + aud check
87
+ const expectedIssSuffix = `/p/${this.http.projectSlug}`
88
+ if (typeof claims.iss !== "string" || !claims.iss.endsWith(expectedIssSuffix)) {
89
+ throw new Error("Issuer mismatch.")
90
+ }
91
+ // aud == project apiKeyPrefix (12 chars). API key first 12 = aud check.
92
+ if (
93
+ typeof claims.aud !== "string" ||
94
+ !this.http.apiKey?.startsWith(claims.aud)
95
+ ) {
96
+ throw new Error("Audience mismatch.")
97
+ }
98
+
99
+ const jwks = await this.fetchJwks()
100
+ const key = jwks.keys.find(
101
+ (k) => (k as { kid?: string }).kid === header.kid,
102
+ ) ?? jwks.keys[0]
103
+ if (!key) throw new Error("No public key in JWKS.")
104
+
105
+ await verifyRsaSignature({
106
+ data: `${headerB64}.${payloadB64}`,
107
+ sigB64,
108
+ jwk: key as JsonWebKey,
109
+ })
110
+
111
+ return claims as never
112
+ }
113
+
114
+ private async fetchJwks(): Promise<{ keys: Record<string, unknown>[] }> {
115
+ if (this.cachedJwks) return this.cachedJwks
116
+ const jwks = await this.http.request<{ keys: Record<string, unknown>[] }>(
117
+ "/jwks.json",
118
+ { method: "GET" },
119
+ )
120
+ this.cachedJwks = jwks
121
+ // 5dk cache — basit setTimeout invalidation
122
+ setTimeout(() => {
123
+ this.cachedJwks = null
124
+ }, 5 * 60 * 1000)
125
+ return jwks
126
+ }
127
+ }
128
+
129
+ // ─── Helpers ─────────────────────────────────────────────────────────────
130
+
131
+ function decodeBase64Url(s: string): string {
132
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/")
133
+ const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4))
134
+ if (typeof atob === "function") {
135
+ const binary = atob(padded + pad)
136
+ const bytes = new Uint8Array(binary.length)
137
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
138
+ return new TextDecoder().decode(bytes)
139
+ }
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ const B = (globalThis as any).Buffer
142
+ if (B) return B.from(padded + pad, "base64").toString("utf8")
143
+ throw new Error("No base64 decoder available")
144
+ }
145
+
146
+ function base64UrlToBytes(s: string): Uint8Array {
147
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/")
148
+ const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4))
149
+ if (typeof atob === "function") {
150
+ const binary = atob(padded + pad)
151
+ const bytes = new Uint8Array(binary.length)
152
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
153
+ return bytes
154
+ }
155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
+ const B = (globalThis as any).Buffer
157
+ if (B) return new Uint8Array(B.from(padded + pad, "base64"))
158
+ throw new Error("No base64 decoder available")
159
+ }
160
+
161
+ async function verifyRsaSignature(input: {
162
+ data: string
163
+ sigB64: string
164
+ jwk: JsonWebKey
165
+ }): Promise<void> {
166
+ // Browser + modern Node (>=18) have crypto.subtle. Tek kod yolu.
167
+ const subtle =
168
+ typeof crypto !== "undefined" && crypto.subtle ? crypto.subtle : null
169
+ if (!subtle) {
170
+ throw new Error("Web Crypto unavailable — upgrade Node >= 18 or run in a browser.")
171
+ }
172
+ const key = await subtle.importKey(
173
+ "jwk",
174
+ input.jwk,
175
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
176
+ false,
177
+ ["verify"],
178
+ )
179
+ // Web Crypto types want ArrayBuffer-backed BufferSource. TypeScript
180
+ // can't prove Uint8Array isn't SharedArrayBuffer-backed (DOM lib edge);
181
+ // bytes are created fresh from base64 decode so ArrayBuffer-safe — cast.
182
+ const sigBytes = base64UrlToBytes(input.sigB64) as Uint8Array
183
+ const dataBytes = new TextEncoder().encode(input.data) as Uint8Array
184
+ const ok = await subtle.verify(
185
+ { name: "RSASSA-PKCS1-v1_5" },
186
+ key,
187
+ sigBytes as unknown as ArrayBuffer,
188
+ dataBytes as unknown as ArrayBuffer,
189
+ )
190
+ if (!ok) throw new Error("Signature mismatch.")
191
+ }