@authdog/node-commons 0.0.22 → 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/dist/index.d.mts CHANGED
@@ -4,16 +4,75 @@ interface PublicKeyPayload {
4
4
  version?: string;
5
5
  region?: "EU" | "US" | "APAC";
6
6
  }
7
+ /**
8
+ * Validates that an identity host is safe to send credentials to: it must be a
9
+ * well-formed `https:` URL whose hostname matches the allowlist and is not a
10
+ * private/loopback address. Throws otherwise.
11
+ */
12
+ declare const assertTrustedIdentityHost: (identityHost: string) => string;
13
+ /**
14
+ * Decodes and fully validates an Authdog public key (`pk_…`). This is the single
15
+ * source of truth for parsing public keys — every framework SDK should use it
16
+ * rather than re-implementing base64/JSON decoding.
17
+ */
18
+ declare const validateAndParsePublicKey: (publicKey: string) => PublicKeyPayload;
19
+ /**
20
+ * @deprecated Use {@link validateAndParsePublicKey}, which additionally
21
+ * validates the decoded payload and identity host. Kept as an alias for
22
+ * backwards compatibility.
23
+ */
7
24
  declare const getPublicKeyPayload: (publicKey: string) => PublicKeyPayload;
8
- declare const validateAndParsePublicKey: (publicKey: string) => any;
9
25
 
10
- declare const parseCookies: (cookieHeader: string | null) => {
26
+ interface ParsedCookie {
11
27
  name: string;
12
28
  value: string;
13
- }[];
29
+ }
30
+ /**
31
+ * Parses a `Cookie` request header into name/value pairs.
32
+ *
33
+ * Splits each pair on the FIRST `=` only — cookie values routinely contain `=`
34
+ * (base64 padding, JWTs), and a naive `split("=")` would silently truncate the
35
+ * value and corrupt the session token. Values are URL-decoded to mirror the
36
+ * `encodeURIComponent` used when the cookie is written.
37
+ */
38
+ declare const parseCookies: (cookieHeader: string | null) => ParsedCookie[];
14
39
 
15
40
  declare const buildSessionKey: (environmentId: string) => string;
16
41
 
17
- declare const fetchUserData: (identityHost: string, environmentId: string, token: string) => Promise<any>;
42
+ interface UserInfoResponse {
43
+ user?: unknown;
44
+ meta?: {
45
+ code?: number;
46
+ [key: string]: unknown;
47
+ };
48
+ [key: string]: unknown;
49
+ }
50
+ /**
51
+ * Fetches user data from the identity host's OIDC `userinfo` endpoint.
52
+ *
53
+ * The `identityHost` is validated against the trusted-host allowlist before the
54
+ * bearer token is sent, preventing SSRF / token exfiltration via a crafted
55
+ * public key. Callers MUST still check `meta.code === 200` (and that `user` is
56
+ * present) before treating the result as authenticated — a `200` HTTP response
57
+ * can carry a non-success envelope.
58
+ */
59
+ declare const fetchUserData: (identityHost: string, environmentId: string, token: string) => Promise<UserInfoResponse>;
60
+ /**
61
+ * Convenience guard: returns true only when the userinfo envelope represents a
62
+ * genuinely authenticated user.
63
+ */
64
+ declare const isAuthenticatedUserInfo: (data: UserInfoResponse | null | undefined) => boolean;
65
+
66
+ /**
67
+ * Returns a safe, same-origin redirect target, falling back to `fallback`
68
+ * (default `/`) for anything that could be an open redirect.
69
+ *
70
+ * Only relative paths are allowed. A value is rejected if it:
71
+ * - is empty / not a string
72
+ * - starts with `//` or `/\` (protocol-relative → off-site)
73
+ * - contains a scheme (`http:`, `javascript:`, `data:`, …)
74
+ * - contains a backslash or control characters (used to bypass naive checks)
75
+ */
76
+ declare const sanitizeRedirectPath: (target: unknown, fallback?: string) => string;
18
77
 
19
- export { type PublicKeyPayload, buildSessionKey, fetchUserData, getPublicKeyPayload, parseCookies, validateAndParsePublicKey };
78
+ export { type ParsedCookie, type PublicKeyPayload, type UserInfoResponse, assertTrustedIdentityHost, buildSessionKey, fetchUserData, getPublicKeyPayload, isAuthenticatedUserInfo, parseCookies, sanitizeRedirectPath, validateAndParsePublicKey };
package/dist/index.d.ts CHANGED
@@ -4,16 +4,75 @@ interface PublicKeyPayload {
4
4
  version?: string;
5
5
  region?: "EU" | "US" | "APAC";
6
6
  }
7
+ /**
8
+ * Validates that an identity host is safe to send credentials to: it must be a
9
+ * well-formed `https:` URL whose hostname matches the allowlist and is not a
10
+ * private/loopback address. Throws otherwise.
11
+ */
12
+ declare const assertTrustedIdentityHost: (identityHost: string) => string;
13
+ /**
14
+ * Decodes and fully validates an Authdog public key (`pk_…`). This is the single
15
+ * source of truth for parsing public keys — every framework SDK should use it
16
+ * rather than re-implementing base64/JSON decoding.
17
+ */
18
+ declare const validateAndParsePublicKey: (publicKey: string) => PublicKeyPayload;
19
+ /**
20
+ * @deprecated Use {@link validateAndParsePublicKey}, which additionally
21
+ * validates the decoded payload and identity host. Kept as an alias for
22
+ * backwards compatibility.
23
+ */
7
24
  declare const getPublicKeyPayload: (publicKey: string) => PublicKeyPayload;
8
- declare const validateAndParsePublicKey: (publicKey: string) => any;
9
25
 
10
- declare const parseCookies: (cookieHeader: string | null) => {
26
+ interface ParsedCookie {
11
27
  name: string;
12
28
  value: string;
13
- }[];
29
+ }
30
+ /**
31
+ * Parses a `Cookie` request header into name/value pairs.
32
+ *
33
+ * Splits each pair on the FIRST `=` only — cookie values routinely contain `=`
34
+ * (base64 padding, JWTs), and a naive `split("=")` would silently truncate the
35
+ * value and corrupt the session token. Values are URL-decoded to mirror the
36
+ * `encodeURIComponent` used when the cookie is written.
37
+ */
38
+ declare const parseCookies: (cookieHeader: string | null) => ParsedCookie[];
14
39
 
15
40
  declare const buildSessionKey: (environmentId: string) => string;
16
41
 
17
- declare const fetchUserData: (identityHost: string, environmentId: string, token: string) => Promise<any>;
42
+ interface UserInfoResponse {
43
+ user?: unknown;
44
+ meta?: {
45
+ code?: number;
46
+ [key: string]: unknown;
47
+ };
48
+ [key: string]: unknown;
49
+ }
50
+ /**
51
+ * Fetches user data from the identity host's OIDC `userinfo` endpoint.
52
+ *
53
+ * The `identityHost` is validated against the trusted-host allowlist before the
54
+ * bearer token is sent, preventing SSRF / token exfiltration via a crafted
55
+ * public key. Callers MUST still check `meta.code === 200` (and that `user` is
56
+ * present) before treating the result as authenticated — a `200` HTTP response
57
+ * can carry a non-success envelope.
58
+ */
59
+ declare const fetchUserData: (identityHost: string, environmentId: string, token: string) => Promise<UserInfoResponse>;
60
+ /**
61
+ * Convenience guard: returns true only when the userinfo envelope represents a
62
+ * genuinely authenticated user.
63
+ */
64
+ declare const isAuthenticatedUserInfo: (data: UserInfoResponse | null | undefined) => boolean;
65
+
66
+ /**
67
+ * Returns a safe, same-origin redirect target, falling back to `fallback`
68
+ * (default `/`) for anything that could be an open redirect.
69
+ *
70
+ * Only relative paths are allowed. A value is rejected if it:
71
+ * - is empty / not a string
72
+ * - starts with `//` or `/\` (protocol-relative → off-site)
73
+ * - contains a scheme (`http:`, `javascript:`, `data:`, …)
74
+ * - contains a backslash or control characters (used to bypass naive checks)
75
+ */
76
+ declare const sanitizeRedirectPath: (target: unknown, fallback?: string) => string;
18
77
 
19
- export { type PublicKeyPayload, buildSessionKey, fetchUserData, getPublicKeyPayload, parseCookies, validateAndParsePublicKey };
78
+ export { type ParsedCookie, type PublicKeyPayload, type UserInfoResponse, assertTrustedIdentityHost, buildSessionKey, fetchUserData, getPublicKeyPayload, isAuthenticatedUserInfo, parseCookies, sanitizeRedirectPath, validateAndParsePublicKey };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var n=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var d=Object.prototype.hasOwnProperty;var y=(r,e)=>{for(var t in e)n(r,t,{get:e[t],enumerable:!0})},P=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of p(e))!d.call(r,i)&&i!==t&&n(r,i,{get:()=>e[i],enumerable:!(o=c(e,i))||o.enumerable});return r};var g=r=>P(n({},"__esModule",{value:!0}),r);var h={};y(h,{buildSessionKey:()=>u,fetchUserData:()=>f,getPublicKeyPayload:()=>s,parseCookies:()=>l,validateAndParsePublicKey:()=>a});module.exports=g(h);var s=r=>{if(!r)throw new Error("Public key is not defined");if(!r.startsWith("pk_"))throw new Error("Invalid public key");try{return JSON.parse(Buffer.from(r.replace("pk_",""),"base64").toString("utf-8"))}catch{throw new Error("Failed to parse public key")}},a=r=>{if(!r)throw new Error("Public key is not defined");if(!r.startsWith("pk_"))throw new Error("Invalid public key");return JSON.parse(Buffer.from(r.replace("pk_",""),"base64").toString("utf-8"))};var l=r=>r?r.split(";").map(e=>{let[t,o]=e.trim().split("=");return{name:t,value:o}}):[];var u=r=>`user_session_${r}`;var f=async(r,e,t)=>{let o=await fetch(`${r}/oidc/${e}/userinfo`,{headers:{authorization:`Bearer ${t}`}});if(!o.ok)throw new Error("Failed to fetch user info");return o.json()};0&&(module.exports={buildSessionKey,fetchUserData,getPublicKeyPayload,parseCookies,validateAndParsePublicKey});
1
+ "use strict";var i=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var g=(e,t)=>{for(var r in t)i(e,r,{get:t[r],enumerable:!0})},I=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of m(t))!P.call(e,n)&&n!==r&&i(e,n,{get:()=>t[n],enumerable:!(o=w(t,n))||o.enumerable});return e};var x=e=>I(i({},"__esModule",{value:!0}),e);var E={};g(E,{assertTrustedIdentityHost:()=>s,buildSessionKey:()=>f,fetchUserData:()=>h,getPublicKeyPayload:()=>d,isAuthenticatedUserInfo:()=>p,parseCookies:()=>c,sanitizeRedirectPath:()=>y,validateAndParsePublicKey:()=>a});module.exports=x(E);var k=["authdog.com","authdog.xyz"],U=e=>{let t=e.toLowerCase();return!!(t==="localhost"||t.endsWith(".localhost")||/^127\./.test(t)||/^10\./.test(t)||/^192\.168\./.test(t)||/^169\.254\./.test(t)||/^172\.(1[6-9]|2\d|3[01])\./.test(t)||t==="::1"||t==="[::1]"||t.startsWith("fc")||t.startsWith("fd")||t.startsWith("[fc")||t.startsWith("[fd"))},b=()=>{let e=(process.env.AUTHDOG_ALLOWED_IDENTITY_HOSTS??"").split(",").map(t=>t.trim().toLowerCase()).filter(Boolean);return[...k,...e]},s=e=>{let t;try{t=new URL(e)}catch{throw new Error("Invalid identity host")}if(t.protocol!=="https:")throw new Error("Identity host must use https");let r=t.hostname.toLowerCase();if(U(r))throw new Error("Untrusted identity host");if(!b().some(n=>r===n||r.endsWith(`.${n}`)))throw new Error("Untrusted identity host");return e.replace(/\/+$/,"")},a=e=>{if(!e)throw new Error("Public key is not defined");if(!e.startsWith("pk_"))throw new Error("Invalid public key");let t;try{t=JSON.parse(Buffer.from(e.replace("pk_",""),"base64").toString("utf-8"))}catch{throw new Error("Failed to parse public key")}if(typeof t!="object"||t===null)throw new Error("Invalid public key payload");let{environmentId:r,identityHost:o}=t;if(typeof r!="string"||r.length===0)throw new Error("Invalid public key: missing environmentId");if(typeof o!="string"||o.length===0)throw new Error("Invalid public key: missing identityHost");let n=s(o);return{...t,identityHost:n}},d=a;var c=e=>e?e.split(";").map(t=>{let r=t.trim();if(!r)return null;let o=r.indexOf("=");if(o===-1)return null;let n=r.slice(0,o).trim();if(!n)return null;let u=r.slice(o+1).trim(),l=u;try{l=decodeURIComponent(u)}catch{}return{name:n,value:l}}).filter(t=>t!==null):[];var f=e=>`user_session_${e}`;var h=async(e,t,r)=>{let o=s(e),n=await fetch(`${o}/oidc/${encodeURIComponent(t)}/userinfo`,{headers:{authorization:`Bearer ${r}`}});if(!n.ok)throw new Error(`Failed to fetch user info (status ${n.status})`);return n.json()},p=e=>!!(e&&e.meta?.code===200&&e.user);var y=(e,t="/")=>typeof e!="string"||e.length===0||!e.startsWith("/")||e.startsWith("//")||e.startsWith("/\\")||/[\\\x00-\x1f]/.test(e)||/^[a-z][a-z0-9+.-]*:/i.test(e)?t:e;0&&(module.exports={assertTrustedIdentityHost,buildSessionKey,fetchUserData,getPublicKeyPayload,isAuthenticatedUserInfo,parseCookies,sanitizeRedirectPath,validateAndParsePublicKey});
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/public-key.ts","../src/cookies.ts","../src/session.ts","../src/identity.ts"],"sourcesContent":["export {\n getPublicKeyPayload,\n validateAndParsePublicKey,\n PublicKeyPayload,\n} from \"./public-key\";\nexport { parseCookies } from \"./cookies\";\nexport { buildSessionKey } from \"./session\";\nexport { fetchUserData } from \"./identity\";\n","export interface PublicKeyPayload {\n environmentId: string;\n identityHost: string;\n version?: string;\n region?: \"EU\" | \"US\" | \"APAC\";\n}\nexport const getPublicKeyPayload = (publicKey: string): PublicKeyPayload => {\n if (!publicKey) {\n throw new Error(\"Public key is not defined\");\n }\n if (!publicKey.startsWith(\"pk_\")) {\n throw new Error(\"Invalid public key\");\n }\n try {\n return JSON.parse(\n Buffer.from(publicKey.replace(\"pk_\", \"\"), \"base64\").toString(\"utf-8\"),\n );\n } catch (e) {\n throw new Error(\"Failed to parse public key\");\n }\n};\n\nexport const validateAndParsePublicKey = (publicKey: string) => {\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 // Decode Base64-encoded publicKey\n return JSON.parse(\n Buffer.from(publicKey.replace(\"pk_\", \"\"), \"base64\").toString(\"utf-8\"),\n );\n};\n","// Function to parse cookies from request\nexport const parseCookies = (cookieHeader: string | null) => {\n if (!cookieHeader) {\n return [];\n }\n\n return cookieHeader.split(\";\").map((cookie) => {\n const [name, value] = cookie.trim().split(\"=\");\n return { name, value };\n });\n};\n","export const buildSessionKey = (environmentId: string) => {\n return `user_session_${environmentId}`;\n};\n","export const fetchUserData = async (\n identityHost: string,\n environmentId: string,\n token: string,\n) => {\n const userData = await fetch(\n `${identityHost}/oidc/${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 userData.json();\n};\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,qBAAAE,EAAA,kBAAAC,EAAA,wBAAAC,EAAA,iBAAAC,EAAA,8BAAAC,IAAA,eAAAC,EAAAP,GCMO,IAAMQ,EAAuBC,GAAwC,CAC1E,GAAI,CAACA,EACH,MAAM,IAAI,MAAM,2BAA2B,EAE7C,GAAI,CAACA,EAAU,WAAW,KAAK,EAC7B,MAAM,IAAI,MAAM,oBAAoB,EAEtC,GAAI,CACF,OAAO,KAAK,MACV,OAAO,KAAKA,EAAU,QAAQ,MAAO,EAAE,EAAG,QAAQ,EAAE,SAAS,OAAO,CACtE,CACF,MAAY,CACV,MAAM,IAAI,MAAM,4BAA4B,CAC9C,CACF,EAEaC,EAA6BD,GAAsB,CAC9D,GAAI,CAACA,EACH,MAAM,IAAI,MAAM,2BAA2B,EAG7C,GAAI,CAACA,EAAU,WAAW,KAAK,EAC7B,MAAM,IAAI,MAAM,oBAAoB,EAItC,OAAO,KAAK,MACV,OAAO,KAAKA,EAAU,QAAQ,MAAO,EAAE,EAAG,QAAQ,EAAE,SAAS,OAAO,CACtE,CACF,EClCO,IAAME,EAAgBC,GACtBA,EAIEA,EAAa,MAAM,GAAG,EAAE,IAAKC,GAAW,CAC7C,GAAM,CAACC,EAAMC,CAAK,EAAIF,EAAO,KAAK,EAAE,MAAM,GAAG,EAC7C,MAAO,CAAE,KAAAC,EAAM,MAAAC,CAAM,CACvB,CAAC,EANQ,CAAC,ECHL,IAAMC,EAAmBC,GACvB,gBAAgBA,CAAa,GCD/B,IAAMC,EAAgB,MAC3BC,EACAC,EACAC,IACG,CACH,IAAMC,EAAW,MAAM,MACrB,GAAGH,CAAY,SAASC,CAAa,YACrC,CACE,QAAS,CACP,cAAe,UAAUC,CAAK,EAChC,CACF,CACF,EAEA,GAAI,CAACC,EAAS,GACZ,MAAM,IAAI,MAAM,2BAA2B,EAG7C,OAAOA,EAAS,KAAK,CACvB","names":["index_exports","__export","buildSessionKey","fetchUserData","getPublicKeyPayload","parseCookies","validateAndParsePublicKey","__toCommonJS","getPublicKeyPayload","publicKey","validateAndParsePublicKey","parseCookies","cookieHeader","cookie","name","value","buildSessionKey","environmentId","fetchUserData","identityHost","environmentId","token","userData"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/public-key.ts","../src/cookies.ts","../src/session.ts","../src/identity.ts","../src/redirects.ts"],"sourcesContent":["export {\n getPublicKeyPayload,\n validateAndParsePublicKey,\n assertTrustedIdentityHost,\n PublicKeyPayload,\n} from \"./public-key\";\nexport { parseCookies, ParsedCookie } from \"./cookies\";\nexport { buildSessionKey } from \"./session\";\nexport { fetchUserData, isAuthenticatedUserInfo, UserInfoResponse } from \"./identity\";\nexport { sanitizeRedirectPath } from \"./redirects\";\n","export interface PublicKeyPayload {\n environmentId: string;\n identityHost: string;\n version?: string;\n region?: \"EU\" | \"US\" | \"APAC\";\n}\n\n/**\n * Hosts the SDK is allowed to send the bearer token to. The identity host is\n * decoded from the (potentially attacker-influenced) public key, so it MUST be\n * validated against an allowlist before it is ever used as a `fetch` target —\n * otherwise a crafted `pk_` could point the SDK (and the token) at an internal\n * address (SSRF) or an attacker-controlled server (token exfiltration + auth\n * bypass).\n *\n * Additional hosts can be allowed via the `AUTHDOG_ALLOWED_IDENTITY_HOSTS`\n * environment variable (comma-separated hostnames), e.g. for self-hosted\n * identity servers.\n */\nconst DEFAULT_ALLOWED_HOST_SUFFIXES = [\"authdog.com\", \"authdog.xyz\"];\n\nconst isPrivateOrLoopbackHost = (hostname: string): boolean => {\n const h = hostname.toLowerCase();\n if (h === \"localhost\" || h.endsWith(\".localhost\")) return true;\n // IPv4 literals in private / loopback / link-local ranges\n if (/^127\\./.test(h)) return true;\n if (/^10\\./.test(h)) return true;\n if (/^192\\.168\\./.test(h)) return true;\n if (/^169\\.254\\./.test(h)) return true; // link-local (cloud metadata)\n if (/^172\\.(1[6-9]|2\\d|3[01])\\./.test(h)) return true;\n // IPv6 loopback / unique-local\n if (h === \"::1\" || h === \"[::1]\") return true;\n if (h.startsWith(\"fc\") || h.startsWith(\"fd\") || h.startsWith(\"[fc\") || h.startsWith(\"[fd\"))\n return true;\n return false;\n};\n\nconst getAllowedHostSuffixes = (): string[] => {\n const extra = (process.env.AUTHDOG_ALLOWED_IDENTITY_HOSTS ?? \"\")\n .split(\",\")\n .map((s) => s.trim().toLowerCase())\n .filter(Boolean);\n return [...DEFAULT_ALLOWED_HOST_SUFFIXES, ...extra];\n};\n\n/**\n * Validates that an identity host is safe to send credentials to: it must be a\n * well-formed `https:` URL whose hostname matches the allowlist and is not a\n * private/loopback address. Throws otherwise.\n */\nexport const assertTrustedIdentityHost = (identityHost: string): string => {\n let url: URL;\n try {\n url = new URL(identityHost);\n } catch {\n throw new Error(\"Invalid identity host\");\n }\n\n if (url.protocol !== \"https:\") {\n throw new Error(\"Identity host must use https\");\n }\n\n const hostname = url.hostname.toLowerCase();\n\n if (isPrivateOrLoopbackHost(hostname)) {\n throw new Error(\"Untrusted identity host\");\n }\n\n const allowed = getAllowedHostSuffixes().some(\n (suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),\n );\n\n if (!allowed) {\n throw new Error(\"Untrusted identity host\");\n }\n\n // Normalize: strip any trailing slash so callers can safely template URLs.\n return identityHost.replace(/\\/+$/, \"\");\n};\n\n/**\n * Decodes and fully validates an Authdog public key (`pk_…`). This is the single\n * source of truth for parsing public keys — every framework SDK should use it\n * rather than re-implementing base64/JSON decoding.\n */\nexport const validateAndParsePublicKey = (publicKey: string): PublicKeyPayload => {\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 let payload: unknown;\n try {\n payload = JSON.parse(\n Buffer.from(publicKey.replace(\"pk_\", \"\"), \"base64\").toString(\"utf-8\"),\n );\n } catch {\n throw new Error(\"Failed to parse public key\");\n }\n\n if (typeof payload !== \"object\" || payload === null) {\n throw new Error(\"Invalid public key payload\");\n }\n\n const { environmentId, identityHost } = payload as Record<string, unknown>;\n\n if (typeof environmentId !== \"string\" || environmentId.length === 0) {\n throw new Error(\"Invalid public key: missing environmentId\");\n }\n\n if (typeof identityHost !== \"string\" || identityHost.length === 0) {\n throw new Error(\"Invalid public key: missing identityHost\");\n }\n\n // Reject any public key pointing at an untrusted identity host.\n const safeIdentityHost = assertTrustedIdentityHost(identityHost);\n\n return { ...(payload as PublicKeyPayload), identityHost: safeIdentityHost };\n};\n\n/**\n * @deprecated Use {@link validateAndParsePublicKey}, which additionally\n * validates the decoded payload and identity host. Kept as an alias for\n * backwards compatibility.\n */\nexport const getPublicKeyPayload = validateAndParsePublicKey;\n","export interface ParsedCookie {\n name: string;\n value: string;\n}\n\n/**\n * Parses a `Cookie` request header into name/value pairs.\n *\n * Splits each pair on the FIRST `=` only — cookie values routinely contain `=`\n * (base64 padding, JWTs), and a naive `split(\"=\")` would silently truncate the\n * value and corrupt the session token. Values are URL-decoded to mirror the\n * `encodeURIComponent` used when the cookie is written.\n */\nexport const parseCookies = (cookieHeader: string | null): ParsedCookie[] => {\n if (!cookieHeader) {\n return [];\n }\n\n return cookieHeader\n .split(\";\")\n .map((cookie): ParsedCookie | null => {\n const trimmed = cookie.trim();\n if (!trimmed) return null;\n\n const idx = trimmed.indexOf(\"=\");\n if (idx === -1) return null;\n\n const name = trimmed.slice(0, idx).trim();\n if (!name) return null;\n\n const rawValue = trimmed.slice(idx + 1).trim();\n let value = rawValue;\n try {\n value = decodeURIComponent(rawValue);\n } catch {\n // Leave the raw value if it isn't valid percent-encoding.\n }\n\n return { name, value };\n })\n .filter((c): c is ParsedCookie => c !== null);\n};\n","export const buildSessionKey = (environmentId: string) => {\n return `user_session_${environmentId}`;\n};\n","import { assertTrustedIdentityHost } from \"./public-key\";\n\nexport interface UserInfoResponse {\n user?: unknown;\n meta?: { code?: number; [key: string]: unknown };\n [key: string]: unknown;\n}\n\n/**\n * Fetches user data from the identity host's OIDC `userinfo` endpoint.\n *\n * The `identityHost` is validated against the trusted-host allowlist before the\n * bearer token is sent, preventing SSRF / token exfiltration via a crafted\n * public key. Callers MUST still check `meta.code === 200` (and that `user` is\n * present) before treating the result as authenticated — a `200` HTTP response\n * can carry a non-success envelope.\n */\nexport const fetchUserData = async (\n identityHost: string,\n environmentId: string,\n token: string,\n): Promise<UserInfoResponse> => {\n const safeHost = assertTrustedIdentityHost(identityHost);\n\n const userData = await fetch(\n `${safeHost}/oidc/${encodeURIComponent(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 (status ${userData.status})`);\n }\n\n return userData.json() as Promise<UserInfoResponse>;\n};\n\n/**\n * Convenience guard: returns true only when the userinfo envelope represents a\n * genuinely authenticated user.\n */\nexport const isAuthenticatedUserInfo = (\n data: UserInfoResponse | null | undefined,\n): boolean => Boolean(data && data.meta?.code === 200 && data.user);\n","/**\n * Returns a safe, same-origin redirect target, falling back to `fallback`\n * (default `/`) for anything that could be an open redirect.\n *\n * Only relative paths are allowed. A value is rejected if it:\n * - is empty / not a string\n * - starts with `//` or `/\\` (protocol-relative → off-site)\n * - contains a scheme (`http:`, `javascript:`, `data:`, …)\n * - contains a backslash or control characters (used to bypass naive checks)\n */\nexport const sanitizeRedirectPath = (\n target: unknown,\n fallback = \"/\",\n): string => {\n if (typeof target !== \"string\" || target.length === 0) {\n return fallback;\n }\n\n // Must be an absolute-path reference, not protocol-relative.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\") || target.startsWith(\"/\\\\\")) {\n return fallback;\n }\n\n // Reject backslashes and control chars that browsers may normalize to `/`.\n if (/[\\\\\\x00-\\x1f]/.test(target)) {\n return fallback;\n }\n\n // Reject anything that parses as having a scheme (e.g. \"/\\t/evil\" tricks).\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {\n return fallback;\n }\n\n return target;\n};\n"],"mappings":"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","names":["index_exports","__export","assertTrustedIdentityHost","buildSessionKey","fetchUserData","getPublicKeyPayload","isAuthenticatedUserInfo","parseCookies","sanitizeRedirectPath","validateAndParsePublicKey","__toCommonJS","DEFAULT_ALLOWED_HOST_SUFFIXES","isPrivateOrLoopbackHost","hostname","h","getAllowedHostSuffixes","extra","s","assertTrustedIdentityHost","identityHost","url","suffix","validateAndParsePublicKey","publicKey","payload","environmentId","safeIdentityHost","getPublicKeyPayload","parseCookies","cookieHeader","cookie","trimmed","idx","name","rawValue","value","c","buildSessionKey","environmentId","fetchUserData","identityHost","environmentId","token","safeHost","assertTrustedIdentityHost","userData","isAuthenticatedUserInfo","data","sanitizeRedirectPath","target","fallback"]}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var i=r=>{if(!r)throw new Error("Public key is not defined");if(!r.startsWith("pk_"))throw new Error("Invalid public key");try{return JSON.parse(Buffer.from(r.replace("pk_",""),"base64").toString("utf-8"))}catch{throw new Error("Failed to parse public key")}},n=r=>{if(!r)throw new Error("Public key is not defined");if(!r.startsWith("pk_"))throw new Error("Invalid public key");return JSON.parse(Buffer.from(r.replace("pk_",""),"base64").toString("utf-8"))};var s=r=>r?r.split(";").map(e=>{let[o,t]=e.trim().split("=");return{name:o,value:t}}):[];var a=r=>`user_session_${r}`;var l=async(r,e,o)=>{let t=await fetch(`${r}/oidc/${e}/userinfo`,{headers:{authorization:`Bearer ${o}`}});if(!t.ok)throw new Error("Failed to fetch user info");return t.json()};export{a as buildSessionKey,l as fetchUserData,i as getPublicKeyPayload,s as parseCookies,n as validateAndParsePublicKey};
1
+ var l=["authdog.com","authdog.xyz"],d=e=>{let t=e.toLowerCase();return!!(t==="localhost"||t.endsWith(".localhost")||/^127\./.test(t)||/^10\./.test(t)||/^192\.168\./.test(t)||/^169\.254\./.test(t)||/^172\.(1[6-9]|2\d|3[01])\./.test(t)||t==="::1"||t==="[::1]"||t.startsWith("fc")||t.startsWith("fd")||t.startsWith("[fc")||t.startsWith("[fd"))},c=()=>{let e=(process.env.AUTHDOG_ALLOWED_IDENTITY_HOSTS??"").split(",").map(t=>t.trim().toLowerCase()).filter(Boolean);return[...l,...e]},s=e=>{let t;try{t=new URL(e)}catch{throw new Error("Invalid identity host")}if(t.protocol!=="https:")throw new Error("Identity host must use https");let r=t.hostname.toLowerCase();if(d(r))throw new Error("Untrusted identity host");if(!c().some(n=>r===n||r.endsWith(`.${n}`)))throw new Error("Untrusted identity host");return e.replace(/\/+$/,"")},u=e=>{if(!e)throw new Error("Public key is not defined");if(!e.startsWith("pk_"))throw new Error("Invalid public key");let t;try{t=JSON.parse(Buffer.from(e.replace("pk_",""),"base64").toString("utf-8"))}catch{throw new Error("Failed to parse public key")}if(typeof t!="object"||t===null)throw new Error("Invalid public key payload");let{environmentId:r,identityHost:o}=t;if(typeof r!="string"||r.length===0)throw new Error("Invalid public key: missing environmentId");if(typeof o!="string"||o.length===0)throw new Error("Invalid public key: missing identityHost");let n=s(o);return{...t,identityHost:n}},f=u;var h=e=>e?e.split(";").map(t=>{let r=t.trim();if(!r)return null;let o=r.indexOf("=");if(o===-1)return null;let n=r.slice(0,o).trim();if(!n)return null;let i=r.slice(o+1).trim(),a=i;try{a=decodeURIComponent(i)}catch{}return{name:n,value:a}}).filter(t=>t!==null):[];var p=e=>`user_session_${e}`;var y=async(e,t,r)=>{let o=s(e),n=await fetch(`${o}/oidc/${encodeURIComponent(t)}/userinfo`,{headers:{authorization:`Bearer ${r}`}});if(!n.ok)throw new Error(`Failed to fetch user info (status ${n.status})`);return n.json()},w=e=>!!(e&&e.meta?.code===200&&e.user);var m=(e,t="/")=>typeof e!="string"||e.length===0||!e.startsWith("/")||e.startsWith("//")||e.startsWith("/\\")||/[\\\x00-\x1f]/.test(e)||/^[a-z][a-z0-9+.-]*:/i.test(e)?t:e;export{s as assertTrustedIdentityHost,p as buildSessionKey,y as fetchUserData,f as getPublicKeyPayload,w as isAuthenticatedUserInfo,h as parseCookies,m as sanitizeRedirectPath,u as validateAndParsePublicKey};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/public-key.ts","../src/cookies.ts","../src/session.ts","../src/identity.ts"],"sourcesContent":["export interface PublicKeyPayload {\n environmentId: string;\n identityHost: string;\n version?: string;\n region?: \"EU\" | \"US\" | \"APAC\";\n}\nexport const getPublicKeyPayload = (publicKey: string): PublicKeyPayload => {\n if (!publicKey) {\n throw new Error(\"Public key is not defined\");\n }\n if (!publicKey.startsWith(\"pk_\")) {\n throw new Error(\"Invalid public key\");\n }\n try {\n return JSON.parse(\n Buffer.from(publicKey.replace(\"pk_\", \"\"), \"base64\").toString(\"utf-8\"),\n );\n } catch (e) {\n throw new Error(\"Failed to parse public key\");\n }\n};\n\nexport const validateAndParsePublicKey = (publicKey: string) => {\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 // Decode Base64-encoded publicKey\n return JSON.parse(\n Buffer.from(publicKey.replace(\"pk_\", \"\"), \"base64\").toString(\"utf-8\"),\n );\n};\n","// Function to parse cookies from request\nexport const parseCookies = (cookieHeader: string | null) => {\n if (!cookieHeader) {\n return [];\n }\n\n return cookieHeader.split(\";\").map((cookie) => {\n const [name, value] = cookie.trim().split(\"=\");\n return { name, value };\n });\n};\n","export const buildSessionKey = (environmentId: string) => {\n return `user_session_${environmentId}`;\n};\n","export const fetchUserData = async (\n identityHost: string,\n environmentId: string,\n token: string,\n) => {\n const userData = await fetch(\n `${identityHost}/oidc/${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 userData.json();\n};\n"],"mappings":"AAMO,IAAMA,EAAuBC,GAAwC,CAC1E,GAAI,CAACA,EACH,MAAM,IAAI,MAAM,2BAA2B,EAE7C,GAAI,CAACA,EAAU,WAAW,KAAK,EAC7B,MAAM,IAAI,MAAM,oBAAoB,EAEtC,GAAI,CACF,OAAO,KAAK,MACV,OAAO,KAAKA,EAAU,QAAQ,MAAO,EAAE,EAAG,QAAQ,EAAE,SAAS,OAAO,CACtE,CACF,MAAY,CACV,MAAM,IAAI,MAAM,4BAA4B,CAC9C,CACF,EAEaC,EAA6BD,GAAsB,CAC9D,GAAI,CAACA,EACH,MAAM,IAAI,MAAM,2BAA2B,EAG7C,GAAI,CAACA,EAAU,WAAW,KAAK,EAC7B,MAAM,IAAI,MAAM,oBAAoB,EAItC,OAAO,KAAK,MACV,OAAO,KAAKA,EAAU,QAAQ,MAAO,EAAE,EAAG,QAAQ,EAAE,SAAS,OAAO,CACtE,CACF,EClCO,IAAME,EAAgBC,GACtBA,EAIEA,EAAa,MAAM,GAAG,EAAE,IAAKC,GAAW,CAC7C,GAAM,CAACC,EAAMC,CAAK,EAAIF,EAAO,KAAK,EAAE,MAAM,GAAG,EAC7C,MAAO,CAAE,KAAAC,EAAM,MAAAC,CAAM,CACvB,CAAC,EANQ,CAAC,ECHL,IAAMC,EAAmBC,GACvB,gBAAgBA,CAAa,GCD/B,IAAMC,EAAgB,MAC3BC,EACAC,EACAC,IACG,CACH,IAAMC,EAAW,MAAM,MACrB,GAAGH,CAAY,SAASC,CAAa,YACrC,CACE,QAAS,CACP,cAAe,UAAUC,CAAK,EAChC,CACF,CACF,EAEA,GAAI,CAACC,EAAS,GACZ,MAAM,IAAI,MAAM,2BAA2B,EAG7C,OAAOA,EAAS,KAAK,CACvB","names":["getPublicKeyPayload","publicKey","validateAndParsePublicKey","parseCookies","cookieHeader","cookie","name","value","buildSessionKey","environmentId","fetchUserData","identityHost","environmentId","token","userData"]}
1
+ {"version":3,"sources":["../src/public-key.ts","../src/cookies.ts","../src/session.ts","../src/identity.ts","../src/redirects.ts"],"sourcesContent":["export interface PublicKeyPayload {\n environmentId: string;\n identityHost: string;\n version?: string;\n region?: \"EU\" | \"US\" | \"APAC\";\n}\n\n/**\n * Hosts the SDK is allowed to send the bearer token to. The identity host is\n * decoded from the (potentially attacker-influenced) public key, so it MUST be\n * validated against an allowlist before it is ever used as a `fetch` target —\n * otherwise a crafted `pk_` could point the SDK (and the token) at an internal\n * address (SSRF) or an attacker-controlled server (token exfiltration + auth\n * bypass).\n *\n * Additional hosts can be allowed via the `AUTHDOG_ALLOWED_IDENTITY_HOSTS`\n * environment variable (comma-separated hostnames), e.g. for self-hosted\n * identity servers.\n */\nconst DEFAULT_ALLOWED_HOST_SUFFIXES = [\"authdog.com\", \"authdog.xyz\"];\n\nconst isPrivateOrLoopbackHost = (hostname: string): boolean => {\n const h = hostname.toLowerCase();\n if (h === \"localhost\" || h.endsWith(\".localhost\")) return true;\n // IPv4 literals in private / loopback / link-local ranges\n if (/^127\\./.test(h)) return true;\n if (/^10\\./.test(h)) return true;\n if (/^192\\.168\\./.test(h)) return true;\n if (/^169\\.254\\./.test(h)) return true; // link-local (cloud metadata)\n if (/^172\\.(1[6-9]|2\\d|3[01])\\./.test(h)) return true;\n // IPv6 loopback / unique-local\n if (h === \"::1\" || h === \"[::1]\") return true;\n if (h.startsWith(\"fc\") || h.startsWith(\"fd\") || h.startsWith(\"[fc\") || h.startsWith(\"[fd\"))\n return true;\n return false;\n};\n\nconst getAllowedHostSuffixes = (): string[] => {\n const extra = (process.env.AUTHDOG_ALLOWED_IDENTITY_HOSTS ?? \"\")\n .split(\",\")\n .map((s) => s.trim().toLowerCase())\n .filter(Boolean);\n return [...DEFAULT_ALLOWED_HOST_SUFFIXES, ...extra];\n};\n\n/**\n * Validates that an identity host is safe to send credentials to: it must be a\n * well-formed `https:` URL whose hostname matches the allowlist and is not a\n * private/loopback address. Throws otherwise.\n */\nexport const assertTrustedIdentityHost = (identityHost: string): string => {\n let url: URL;\n try {\n url = new URL(identityHost);\n } catch {\n throw new Error(\"Invalid identity host\");\n }\n\n if (url.protocol !== \"https:\") {\n throw new Error(\"Identity host must use https\");\n }\n\n const hostname = url.hostname.toLowerCase();\n\n if (isPrivateOrLoopbackHost(hostname)) {\n throw new Error(\"Untrusted identity host\");\n }\n\n const allowed = getAllowedHostSuffixes().some(\n (suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),\n );\n\n if (!allowed) {\n throw new Error(\"Untrusted identity host\");\n }\n\n // Normalize: strip any trailing slash so callers can safely template URLs.\n return identityHost.replace(/\\/+$/, \"\");\n};\n\n/**\n * Decodes and fully validates an Authdog public key (`pk_…`). This is the single\n * source of truth for parsing public keys — every framework SDK should use it\n * rather than re-implementing base64/JSON decoding.\n */\nexport const validateAndParsePublicKey = (publicKey: string): PublicKeyPayload => {\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 let payload: unknown;\n try {\n payload = JSON.parse(\n Buffer.from(publicKey.replace(\"pk_\", \"\"), \"base64\").toString(\"utf-8\"),\n );\n } catch {\n throw new Error(\"Failed to parse public key\");\n }\n\n if (typeof payload !== \"object\" || payload === null) {\n throw new Error(\"Invalid public key payload\");\n }\n\n const { environmentId, identityHost } = payload as Record<string, unknown>;\n\n if (typeof environmentId !== \"string\" || environmentId.length === 0) {\n throw new Error(\"Invalid public key: missing environmentId\");\n }\n\n if (typeof identityHost !== \"string\" || identityHost.length === 0) {\n throw new Error(\"Invalid public key: missing identityHost\");\n }\n\n // Reject any public key pointing at an untrusted identity host.\n const safeIdentityHost = assertTrustedIdentityHost(identityHost);\n\n return { ...(payload as PublicKeyPayload), identityHost: safeIdentityHost };\n};\n\n/**\n * @deprecated Use {@link validateAndParsePublicKey}, which additionally\n * validates the decoded payload and identity host. Kept as an alias for\n * backwards compatibility.\n */\nexport const getPublicKeyPayload = validateAndParsePublicKey;\n","export interface ParsedCookie {\n name: string;\n value: string;\n}\n\n/**\n * Parses a `Cookie` request header into name/value pairs.\n *\n * Splits each pair on the FIRST `=` only — cookie values routinely contain `=`\n * (base64 padding, JWTs), and a naive `split(\"=\")` would silently truncate the\n * value and corrupt the session token. Values are URL-decoded to mirror the\n * `encodeURIComponent` used when the cookie is written.\n */\nexport const parseCookies = (cookieHeader: string | null): ParsedCookie[] => {\n if (!cookieHeader) {\n return [];\n }\n\n return cookieHeader\n .split(\";\")\n .map((cookie): ParsedCookie | null => {\n const trimmed = cookie.trim();\n if (!trimmed) return null;\n\n const idx = trimmed.indexOf(\"=\");\n if (idx === -1) return null;\n\n const name = trimmed.slice(0, idx).trim();\n if (!name) return null;\n\n const rawValue = trimmed.slice(idx + 1).trim();\n let value = rawValue;\n try {\n value = decodeURIComponent(rawValue);\n } catch {\n // Leave the raw value if it isn't valid percent-encoding.\n }\n\n return { name, value };\n })\n .filter((c): c is ParsedCookie => c !== null);\n};\n","export const buildSessionKey = (environmentId: string) => {\n return `user_session_${environmentId}`;\n};\n","import { assertTrustedIdentityHost } from \"./public-key\";\n\nexport interface UserInfoResponse {\n user?: unknown;\n meta?: { code?: number; [key: string]: unknown };\n [key: string]: unknown;\n}\n\n/**\n * Fetches user data from the identity host's OIDC `userinfo` endpoint.\n *\n * The `identityHost` is validated against the trusted-host allowlist before the\n * bearer token is sent, preventing SSRF / token exfiltration via a crafted\n * public key. Callers MUST still check `meta.code === 200` (and that `user` is\n * present) before treating the result as authenticated — a `200` HTTP response\n * can carry a non-success envelope.\n */\nexport const fetchUserData = async (\n identityHost: string,\n environmentId: string,\n token: string,\n): Promise<UserInfoResponse> => {\n const safeHost = assertTrustedIdentityHost(identityHost);\n\n const userData = await fetch(\n `${safeHost}/oidc/${encodeURIComponent(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 (status ${userData.status})`);\n }\n\n return userData.json() as Promise<UserInfoResponse>;\n};\n\n/**\n * Convenience guard: returns true only when the userinfo envelope represents a\n * genuinely authenticated user.\n */\nexport const isAuthenticatedUserInfo = (\n data: UserInfoResponse | null | undefined,\n): boolean => Boolean(data && data.meta?.code === 200 && data.user);\n","/**\n * Returns a safe, same-origin redirect target, falling back to `fallback`\n * (default `/`) for anything that could be an open redirect.\n *\n * Only relative paths are allowed. A value is rejected if it:\n * - is empty / not a string\n * - starts with `//` or `/\\` (protocol-relative → off-site)\n * - contains a scheme (`http:`, `javascript:`, `data:`, …)\n * - contains a backslash or control characters (used to bypass naive checks)\n */\nexport const sanitizeRedirectPath = (\n target: unknown,\n fallback = \"/\",\n): string => {\n if (typeof target !== \"string\" || target.length === 0) {\n return fallback;\n }\n\n // Must be an absolute-path reference, not protocol-relative.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\") || target.startsWith(\"/\\\\\")) {\n return fallback;\n }\n\n // Reject backslashes and control chars that browsers may normalize to `/`.\n if (/[\\\\\\x00-\\x1f]/.test(target)) {\n return fallback;\n }\n\n // Reject anything that parses as having a scheme (e.g. \"/\\t/evil\" tricks).\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {\n return fallback;\n }\n\n return target;\n};\n"],"mappings":"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","names":["DEFAULT_ALLOWED_HOST_SUFFIXES","isPrivateOrLoopbackHost","hostname","h","getAllowedHostSuffixes","extra","s","assertTrustedIdentityHost","identityHost","url","suffix","validateAndParsePublicKey","publicKey","payload","environmentId","safeIdentityHost","getPublicKeyPayload","parseCookies","cookieHeader","cookie","trimmed","idx","name","rawValue","value","c","buildSessionKey","environmentId","fetchUserData","identityHost","environmentId","token","safeHost","assertTrustedIdentityHost","userData","isAuthenticatedUserInfo","data","sanitizeRedirectPath","target","fallback"]}
package/package.json CHANGED
@@ -1,38 +1,48 @@
1
1
  {
2
- "name": "@authdog/node-commons",
3
- "version": "0.0.22",
4
- "description": "Authdog Next.js 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"
2
+ "name": "@authdog/node-commons",
3
+ "version": "0.2.0",
4
+ "description": "Authdog Next.js 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/node-commons"
24
+ },
25
+ "homepage": "https://github.com/authdog-labs/web-sdk/tree/main/packages/node-commons#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
+ "devDependencies": {
37
+ "@types/node": "^22.14.1",
38
+ "dotenv": "^16.4.7",
39
+ "prettier": "^3.4.2",
40
+ "tsup": "^8.3.5",
41
+ "typescript": "^5.7.2",
42
+ "vitest": "^2.1.8"
43
+ },
44
+ "publishConfig": {
45
+ "registry": "https://registry.npmjs.org/",
46
+ "access": "public"
14
47
  }
15
- },
16
- "files": [
17
- "dist/"
18
- ],
19
- "devDependencies": {
20
- "@types/node": "^22.14.1",
21
- "dotenv": "^16.4.7",
22
- "prettier": "^3.4.2",
23
- "tsup": "^8.3.5",
24
- "typescript": "^5.7.2",
25
- "vitest": "^2.1.8"
26
- },
27
- "publishConfig": {
28
- "registry": "https://registry.npmjs.org/",
29
- "access": "public"
30
- },
31
- "scripts": {
32
- "format": "prettier --config .prettierrc.json --write \"**/*.{ts,md}\"",
33
- "type-check": "tsc",
34
- "clean": "rm -rf dist",
35
- "build": "pnpm clean && tsup",
36
- "ship": "pnpm build && pnpm publish --access public --no-git-checks"
37
- }
38
48
  }