@storyshelf/auth 0.5.3

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -0
  3. package/dist/auth-tables-pg.d.mts +1971 -0
  4. package/dist/auth-tables-pg.mjs +167 -0
  5. package/dist/auth-tables-pg.mjs.map +1 -0
  6. package/dist/auth-tables.d.mts +1092 -0
  7. package/dist/auth-tables.mjs +122 -0
  8. package/dist/auth-tables.mjs.map +1 -0
  9. package/dist/bootstrap.d.mts +15 -0
  10. package/dist/bootstrap.mjs +40 -0
  11. package/dist/bootstrap.mjs.map +1 -0
  12. package/dist/config.d.mts +27 -0
  13. package/dist/config.mjs +204 -0
  14. package/dist/config.mjs.map +1 -0
  15. package/dist/db-bridge.d.mts +20 -0
  16. package/dist/db-bridge.mjs +208 -0
  17. package/dist/db-bridge.mjs.map +1 -0
  18. package/dist/db-values.mjs +41 -0
  19. package/dist/db-values.mjs.map +1 -0
  20. package/dist/db-where.mjs +85 -0
  21. package/dist/db-where.mjs.map +1 -0
  22. package/dist/engine.d.mts +208 -0
  23. package/dist/engine.mjs +444 -0
  24. package/dist/engine.mjs.map +1 -0
  25. package/dist/groups.d.mts +54 -0
  26. package/dist/groups.mjs +129 -0
  27. package/dist/groups.mjs.map +1 -0
  28. package/dist/index.d.mts +18 -0
  29. package/dist/index.mjs +17 -0
  30. package/dist/invites.d.mts +41 -0
  31. package/dist/invites.mjs +225 -0
  32. package/dist/invites.mjs.map +1 -0
  33. package/dist/passkeys.d.mts +21 -0
  34. package/dist/passkeys.mjs +27 -0
  35. package/dist/passkeys.mjs.map +1 -0
  36. package/dist/presets/enterprise.d.mts +24 -0
  37. package/dist/presets/enterprise.mjs +44 -0
  38. package/dist/presets/enterprise.mjs.map +1 -0
  39. package/dist/presets/index.d.mts +5 -0
  40. package/dist/presets/social.d.mts +34 -0
  41. package/dist/presets/social.mjs +60 -0
  42. package/dist/presets/social.mjs.map +1 -0
  43. package/dist/presets/sso.d.mts +72 -0
  44. package/dist/presets/sso.mjs +136 -0
  45. package/dist/presets/sso.mjs.map +1 -0
  46. package/dist/sessions.d.mts +23 -0
  47. package/dist/sessions.mjs +51 -0
  48. package/dist/sessions.mjs.map +1 -0
  49. package/package.json +82 -0
@@ -0,0 +1,34 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { ShelfSocialProvider } from "../engine.mjs";
4
+ //#region src/presets/social.d.ts
5
+ /** Client credentials shared by the OAuth social presets. */
6
+ interface SocialClientOptions {
7
+ clientId: string;
8
+ clientSecret?: string;
9
+ label?: string;
10
+ }
11
+ /** GitHub social login (`github`). */
12
+ declare function githubPreset(options: SocialClientOptions): ShelfSocialProvider;
13
+ /** GitLab social login (`gitlab`; `issuer` for self-managed instances). */
14
+ declare function gitlabPreset(options: SocialClientOptions & {
15
+ issuer?: string;
16
+ }): ShelfSocialProvider;
17
+ /** Google social login (`google`). */
18
+ declare function googlePreset(options: SocialClientOptions): ShelfSocialProvider;
19
+ /** Microsoft Entra ID social login (`microsoft`; `tenantId` or `common`). */
20
+ declare function entraPreset(options: SocialClientOptions & {
21
+ tenantId?: string;
22
+ }): ShelfSocialProvider;
23
+ /** Amazon Cognito User Pool social login (`cognito`). */
24
+ declare function cognitoPreset(options: {
25
+ domain: string;
26
+ region: string;
27
+ userPoolId: string;
28
+ clientId: string;
29
+ clientSecret?: string;
30
+ label?: string;
31
+ }): ShelfSocialProvider;
32
+ //#endregion
33
+ export { SocialClientOptions, cognitoPreset, entraPreset, githubPreset, gitlabPreset, googlePreset };
34
+ //# sourceMappingURL=social.d.mts.map
@@ -0,0 +1,60 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/presets/social.ts
5
+ /** GitHub social login (`github`). */
6
+ function githubPreset(options) {
7
+ return {
8
+ id: "github",
9
+ label: options.label ?? "GitHub",
10
+ clientId: options.clientId,
11
+ clientSecret: options.clientSecret
12
+ };
13
+ }
14
+ /** GitLab social login (`gitlab`; `issuer` for self-managed instances). */
15
+ function gitlabPreset(options) {
16
+ return {
17
+ id: "gitlab",
18
+ label: options.label ?? "GitLab",
19
+ clientId: options.clientId,
20
+ clientSecret: options.clientSecret,
21
+ extra: { issuer: options.issuer }
22
+ };
23
+ }
24
+ /** Google social login (`google`). */
25
+ function googlePreset(options) {
26
+ return {
27
+ id: "google",
28
+ label: options.label ?? "Google",
29
+ clientId: options.clientId,
30
+ clientSecret: options.clientSecret
31
+ };
32
+ }
33
+ /** Microsoft Entra ID social login (`microsoft`; `tenantId` or `common`). */
34
+ function entraPreset(options) {
35
+ return {
36
+ id: "microsoft",
37
+ label: options.label ?? "Microsoft",
38
+ clientId: options.clientId,
39
+ clientSecret: options.clientSecret,
40
+ extra: { tenantId: options.tenantId }
41
+ };
42
+ }
43
+ /** Amazon Cognito User Pool social login (`cognito`). */
44
+ function cognitoPreset(options) {
45
+ return {
46
+ id: "cognito",
47
+ label: options.label ?? "Cognito",
48
+ clientId: options.clientId,
49
+ clientSecret: options.clientSecret,
50
+ extra: {
51
+ domain: options.domain,
52
+ region: options.region,
53
+ userPoolId: options.userPoolId
54
+ }
55
+ };
56
+ }
57
+ //#endregion
58
+ export { cognitoPreset, entraPreset, githubPreset, gitlabPreset, googlePreset };
59
+
60
+ //# sourceMappingURL=social.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"social.mjs","names":[],"sources":["../../src/presets/social.ts"],"sourcesContent":["/**\n * Native social provider recipes: option bags for Better Auth built-ins.\n *\n * Each preset returns a {@link ShelfSocialProvider} for\n * `createShelfAuth({ social })`. Native providers own their protocol\n * (PKCE, token exchange, profile mapping) — the shelf only supplies\n * credentials, so these recipes are pure config with no network behavior.\n */\nimport type { ShelfSocialProvider } from \"../engine.ts\";\n\n/** Client credentials shared by the OAuth social presets. */\nexport interface SocialClientOptions {\n clientId: string;\n clientSecret?: string;\n label?: string;\n}\n\n/** GitHub social login (`github`). */\nexport function githubPreset(options: SocialClientOptions): ShelfSocialProvider {\n return {\n id: \"github\",\n label: options.label ?? \"GitHub\",\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n };\n}\n\n/** GitLab social login (`gitlab`; `issuer` for self-managed instances). */\nexport function gitlabPreset(\n options: SocialClientOptions & { issuer?: string },\n): ShelfSocialProvider {\n return {\n id: \"gitlab\",\n label: options.label ?? \"GitLab\",\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n extra: { issuer: options.issuer },\n };\n}\n\n/** Google social login (`google`). */\nexport function googlePreset(options: SocialClientOptions): ShelfSocialProvider {\n return {\n id: \"google\",\n label: options.label ?? \"Google\",\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n };\n}\n\n/** Microsoft Entra ID social login (`microsoft`; `tenantId` or `common`). */\nexport function entraPreset(\n options: SocialClientOptions & { tenantId?: string },\n): ShelfSocialProvider {\n return {\n id: \"microsoft\",\n label: options.label ?? \"Microsoft\",\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n extra: { tenantId: options.tenantId },\n };\n}\n\n/** Amazon Cognito User Pool social login (`cognito`). */\nexport function cognitoPreset(options: {\n domain: string;\n region: string;\n userPoolId: string;\n clientId: string;\n clientSecret?: string;\n label?: string;\n}): ShelfSocialProvider {\n return {\n id: \"cognito\",\n label: options.label ?? \"Cognito\",\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n extra: { domain: options.domain, region: options.region, userPoolId: options.userPoolId },\n };\n}\n"],"mappings":";;;;;AAkBA,SAAgB,aAAa,SAAmD;CAC9E,OAAO;EACL,IAAI;EACJ,OAAO,QAAQ,SAAS;EACxB,UAAU,QAAQ;EAClB,cAAc,QAAQ;CACxB;AACF;;AAGA,SAAgB,aACd,SACqB;CACrB,OAAO;EACL,IAAI;EACJ,OAAO,QAAQ,SAAS;EACxB,UAAU,QAAQ;EAClB,cAAc,QAAQ;EACtB,OAAO,EAAE,QAAQ,QAAQ,OAAO;CAClC;AACF;;AAGA,SAAgB,aAAa,SAAmD;CAC9E,OAAO;EACL,IAAI;EACJ,OAAO,QAAQ,SAAS;EACxB,UAAU,QAAQ;EAClB,cAAc,QAAQ;CACxB;AACF;;AAGA,SAAgB,YACd,SACqB;CACrB,OAAO;EACL,IAAI;EACJ,OAAO,QAAQ,SAAS;EACxB,UAAU,QAAQ;EAClB,cAAc,QAAQ;EACtB,OAAO,EAAE,UAAU,QAAQ,SAAS;CACtC;AACF;;AAGA,SAAgB,cAAc,SAON;CACtB,OAAO;EACL,IAAI;EACJ,OAAO,QAAQ,SAAS;EACxB,UAAU,QAAQ;EAClB,cAAc,QAAQ;EACtB,OAAO;GAAE,QAAQ,QAAQ;GAAQ,QAAQ,QAAQ;GAAQ,YAAY,QAAQ;EAAW;CAC1F;AACF"}
@@ -0,0 +1,72 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { ShelfSSOProvider } from "../engine.mjs";
4
+ //#region src/presets/sso.d.ts
5
+ /** Client credentials shared by the OIDC SSO presets. */
6
+ interface SSOClientOptions {
7
+ /** Email domain routing key (e.g. "acme.example"). */
8
+ domain: string;
9
+ clientId: string;
10
+ clientSecret?: string;
11
+ label?: string;
12
+ /** Stable provider id. Defaults to the preset name. */
13
+ id?: string;
14
+ /** OIDC claim name(s) carrying groups. Defaults to `["groups"]`. */
15
+ groupsClaim?: string | string[];
16
+ /** Exact group names granting the site `admin` role (full reconcile). */
17
+ adminGroups?: string[];
18
+ }
19
+ /** Group sync shared by the SAML preset. */
20
+ interface SAMLGroupOptions {
21
+ /**
22
+ * SAML attribute name carrying groups (surfaced under the same name).
23
+ * Defaults to `"groups"`.
24
+ */
25
+ groupsAttribute?: string;
26
+ /** Exact group names granting the site `admin` role (full reconcile). */
27
+ adminGroups?: string[];
28
+ }
29
+ /** IdP metadata for the SAML preset (XML preferred, explicit fields otherwise). */
30
+ interface SAMLIdPOptions {
31
+ /** IdP SSO URL (required by the plugin type; unused when `metadataXml` is set). */
32
+ entryPoint: string;
33
+ /** IdP metadata XML (preferred: carries entity ID, endpoints, and certs). */
34
+ metadataXml?: string;
35
+ /** IdP entity ID (required without `metadataXml`). */
36
+ entityID?: string;
37
+ /** IdP signing cert(s), PEM (ignored when `metadataXml` is set). */
38
+ cert?: string | string[];
39
+ }
40
+ /** Generic SAML recipe (any IdP with an SSO URL or metadata XML). */
41
+ declare function samlPreset(options: SAMLIdPOptions & SAMLGroupOptions & {
42
+ domain: string;
43
+ /** SP entity ID (our audience). */
44
+ issuer: string;
45
+ label?: string;
46
+ id?: string;
47
+ }): ShelfSSOProvider;
48
+ /** Keycloak realm SSO (OIDC discovery over the realm URL). */
49
+ declare function ssoKeycloakPreset(realmUrl: string, options: SSOClientOptions): ShelfSSOProvider;
50
+ /** Okta SSO (custom authorization server, OIDC discovery). */
51
+ declare function ssoOktaPreset(options: SSOClientOptions & {
52
+ authorizationServerId: string;
53
+ }): ShelfSSOProvider;
54
+ /** Auth0 SSO (OIDC discovery over the tenant domain). */
55
+ declare function ssoAuth0Preset(options: SSOClientOptions & {
56
+ tenant: string;
57
+ }): ShelfSSOProvider;
58
+ /** Google Workspace SSO (OIDC discovery; `domain` is the Workspace domain). */
59
+ declare function ssoGooglePreset(options: SSOClientOptions): ShelfSSOProvider;
60
+ /** Microsoft Entra ID SSO (OIDC discovery over the tenant issuer). */
61
+ declare function ssoEntraPreset(options: SSOClientOptions & {
62
+ tenantId?: string;
63
+ }): ShelfSSOProvider;
64
+ /** IdP callback URLs for one SSO provider (hand these to the IdP admin). */
65
+ declare function ssoCallbackUrls(baseURL: string, providerId: string): {
66
+ acs: string;
67
+ metadata: string;
68
+ oidcCallback: string;
69
+ };
70
+ //#endregion
71
+ export { SAMLGroupOptions, SAMLIdPOptions, SSOClientOptions, samlPreset, ssoAuth0Preset, ssoCallbackUrls, ssoEntraPreset, ssoGooglePreset, ssoKeycloakPreset, ssoOktaPreset };
72
+ //# sourceMappingURL=sso.d.mts.map
@@ -0,0 +1,136 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/presets/sso.ts
5
+ /** Build the `groups` entry when either knob is set (else no group reads). */
6
+ function groupSyncOf(options) {
7
+ if (options.claim === void 0 && options.admins === void 0) return;
8
+ return {
9
+ ...options.claim === void 0 ? {} : { claim: options.claim },
10
+ ...options.admins === void 0 ? {} : { admins: options.admins }
11
+ };
12
+ }
13
+ /** Resolve the IdP authority, failing fast when it is under-specified. */
14
+ function samlAuthority(options) {
15
+ if (options.metadataXml !== void 0) return {
16
+ metadataXml: options.metadataXml,
17
+ entityID: options.entityID
18
+ };
19
+ if (options.entityID === void 0) throw new Error("Shelf SSO SAML needs entityID or metadataXml");
20
+ return { entityID: options.entityID };
21
+ }
22
+ /** Generic SAML recipe (any IdP with an SSO URL or metadata XML). */
23
+ function samlPreset(options) {
24
+ const authority = samlAuthority(options);
25
+ const syncing = options.adminGroups !== void 0 || options.groupsAttribute !== void 0;
26
+ const attribute = options.groupsAttribute ?? "groups";
27
+ return {
28
+ id: options.id ?? "saml",
29
+ label: options.label ?? "SSO",
30
+ domain: options.domain,
31
+ saml: {
32
+ issuer: options.issuer,
33
+ entryPoint: options.entryPoint,
34
+ ...options.cert === void 0 ? {} : { cert: options.cert },
35
+ ...authority.metadataXml === void 0 ? {} : { metadataXml: authority.metadataXml },
36
+ ...authority.entityID === void 0 ? {} : { entityID: authority.entityID }
37
+ },
38
+ ...syncing ? { groups: {
39
+ claim: [attribute],
40
+ ...options.adminGroups === void 0 ? {} : { admins: options.adminGroups }
41
+ } } : {}
42
+ };
43
+ }
44
+ /** Keycloak realm SSO (OIDC discovery over the realm URL). */
45
+ function ssoKeycloakPreset(realmUrl, options) {
46
+ return {
47
+ id: options.id ?? "keycloak",
48
+ label: options.label ?? "Keycloak",
49
+ domain: options.domain,
50
+ oidc: {
51
+ issuer: realmUrl,
52
+ clientId: options.clientId,
53
+ clientSecret: options.clientSecret
54
+ },
55
+ ...oidcGroupsOf(options)
56
+ };
57
+ }
58
+ /** OIDC `groups` entry for the client-option presets. */
59
+ function oidcGroupsOf(options) {
60
+ const groups = groupSyncOf({
61
+ claim: options.groupsClaim,
62
+ admins: options.adminGroups
63
+ });
64
+ return groups === void 0 ? {} : { groups };
65
+ }
66
+ /** Okta SSO (custom authorization server, OIDC discovery). */
67
+ function ssoOktaPreset(options) {
68
+ const issuer = `https://${options.domain}/oauth2/${options.authorizationServerId}`;
69
+ return {
70
+ id: options.id ?? "okta",
71
+ label: options.label ?? "Okta",
72
+ domain: options.domain,
73
+ oidc: {
74
+ issuer,
75
+ clientId: options.clientId,
76
+ clientSecret: options.clientSecret
77
+ },
78
+ ...oidcGroupsOf(options)
79
+ };
80
+ }
81
+ /** Auth0 SSO (OIDC discovery over the tenant domain). */
82
+ function ssoAuth0Preset(options) {
83
+ return {
84
+ id: options.id ?? "auth0",
85
+ label: options.label ?? "Auth0",
86
+ domain: options.domain,
87
+ oidc: {
88
+ issuer: `https://${options.tenant}/`,
89
+ clientId: options.clientId,
90
+ clientSecret: options.clientSecret
91
+ },
92
+ ...oidcGroupsOf(options)
93
+ };
94
+ }
95
+ /** Google Workspace SSO (OIDC discovery; `domain` is the Workspace domain). */
96
+ function ssoGooglePreset(options) {
97
+ return {
98
+ id: options.id ?? "google-workspace",
99
+ label: options.label ?? "Google",
100
+ domain: options.domain,
101
+ oidc: {
102
+ issuer: "https://accounts.google.com",
103
+ clientId: options.clientId,
104
+ clientSecret: options.clientSecret
105
+ },
106
+ ...oidcGroupsOf(options)
107
+ };
108
+ }
109
+ /** Microsoft Entra ID SSO (OIDC discovery over the tenant issuer). */
110
+ function ssoEntraPreset(options) {
111
+ const tenant = options.tenantId ?? "common";
112
+ return {
113
+ id: options.id ?? "entra",
114
+ label: options.label ?? "Microsoft",
115
+ domain: options.domain,
116
+ oidc: {
117
+ issuer: `https://login.microsoftonline.com/${tenant}/v2.0`,
118
+ clientId: options.clientId,
119
+ clientSecret: options.clientSecret
120
+ },
121
+ ...oidcGroupsOf(options)
122
+ };
123
+ }
124
+ /** IdP callback URLs for one SSO provider (hand these to the IdP admin). */
125
+ function ssoCallbackUrls(baseURL, providerId) {
126
+ const root = baseURL.replace(/\/+$/u, "");
127
+ return {
128
+ acs: `${root}/api/auth/sso/saml2/sp/acs/${providerId}`,
129
+ metadata: `${root}/api/auth/sso/saml2/sp/metadata?providerId=${providerId}`,
130
+ oidcCallback: `${root}/api/auth/sso/callback/${providerId}`
131
+ };
132
+ }
133
+ //#endregion
134
+ export { samlPreset, ssoAuth0Preset, ssoCallbackUrls, ssoEntraPreset, ssoGooglePreset, ssoKeycloakPreset, ssoOktaPreset };
135
+
136
+ //# sourceMappingURL=sso.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sso.mjs","names":[],"sources":["../../src/presets/sso.ts"],"sourcesContent":["/**\n * SSO recipes: code-driven provider entries for the Better Auth SSO plugin.\n *\n * Each preset returns a {@link ShelfSSOProvider} for\n * `createShelfAuth({ sso: { providers } })`. Providers resolve by email\n * domain at sign-in; OIDC entries use IdP discovery, SAML entries use\n * samlify through the plugin (never hand-rolled XML).\n *\n * IdP-side configuration per provider (replace `{baseURL}` and `{id}`):\n * - ACS (reply) URL: `{baseURL}/api/auth/sso/saml2/sp/acs/{id}`\n * - SP metadata: `{baseURL}/api/auth/sso/saml2/sp/metadata?providerId={id}`\n * - OIDC callback: `{baseURL}/api/auth/sso/callback/{id}`\n */\nimport type { ShelfSSOGroupSync, ShelfSSOProvider } from \"../engine.ts\";\n\n/** Client credentials shared by the OIDC SSO presets. */\nexport interface SSOClientOptions {\n /** Email domain routing key (e.g. \"acme.example\"). */\n domain: string;\n clientId: string;\n clientSecret?: string;\n label?: string;\n /** Stable provider id. Defaults to the preset name. */\n id?: string;\n /** OIDC claim name(s) carrying groups. Defaults to `[\"groups\"]`. */\n groupsClaim?: string | string[];\n /** Exact group names granting the site `admin` role (full reconcile). */\n adminGroups?: string[];\n}\n\n/** Group sync shared by the SAML preset. */\nexport interface SAMLGroupOptions {\n /**\n * SAML attribute name carrying groups (surfaced under the same name).\n * Defaults to `\"groups\"`.\n */\n groupsAttribute?: string;\n /** Exact group names granting the site `admin` role (full reconcile). */\n adminGroups?: string[];\n}\n\n/** Build the `groups` entry when either knob is set (else no group reads). */\nfunction groupSyncOf(options: {\n claim?: string | string[];\n admins?: string[];\n}): ShelfSSOGroupSync | undefined {\n if (options.claim === undefined && options.admins === undefined) {\n return undefined;\n }\n return {\n ...(options.claim === undefined ? {} : { claim: options.claim }),\n ...(options.admins === undefined ? {} : { admins: options.admins }),\n };\n}\n\n/** IdP metadata for the SAML preset (XML preferred, explicit fields otherwise). */\nexport interface SAMLIdPOptions {\n /** IdP SSO URL (required by the plugin type; unused when `metadataXml` is set). */\n entryPoint: string;\n /** IdP metadata XML (preferred: carries entity ID, endpoints, and certs). */\n metadataXml?: string;\n /** IdP entity ID (required without `metadataXml`). */\n entityID?: string;\n /** IdP signing cert(s), PEM (ignored when `metadataXml` is set). */\n cert?: string | string[];\n}\n\n/** Resolve the IdP authority, failing fast when it is under-specified. */\nfunction samlAuthority(options: SAMLIdPOptions): { metadataXml?: string; entityID?: string } {\n if (options.metadataXml !== undefined) {\n return { metadataXml: options.metadataXml, entityID: options.entityID };\n }\n if (options.entityID === undefined) {\n throw new Error(\"Shelf SSO SAML needs entityID or metadataXml\");\n }\n return { entityID: options.entityID };\n}\n\n/** Generic SAML recipe (any IdP with an SSO URL or metadata XML). */\nexport function samlPreset(\n options: SAMLIdPOptions &\n SAMLGroupOptions & {\n domain: string;\n /** SP entity ID (our audience). */\n issuer: string;\n label?: string;\n id?: string;\n },\n): ShelfSSOProvider {\n const authority = samlAuthority(options);\n const syncing = options.adminGroups !== undefined || options.groupsAttribute !== undefined;\n const attribute = options.groupsAttribute ?? \"groups\";\n return {\n id: options.id ?? \"saml\",\n label: options.label ?? \"SSO\",\n domain: options.domain,\n saml: {\n issuer: options.issuer,\n entryPoint: options.entryPoint,\n ...(options.cert === undefined ? {} : { cert: options.cert }),\n ...(authority.metadataXml === undefined ? {} : { metadataXml: authority.metadataXml }),\n ...(authority.entityID === undefined ? {} : { entityID: authority.entityID }),\n },\n ...(syncing\n ? {\n groups: {\n claim: [attribute],\n ...(options.adminGroups === undefined ? {} : { admins: options.adminGroups }),\n },\n }\n : {}),\n };\n}\n\n/** Keycloak realm SSO (OIDC discovery over the realm URL). */\nexport function ssoKeycloakPreset(realmUrl: string, options: SSOClientOptions): ShelfSSOProvider {\n return {\n id: options.id ?? \"keycloak\",\n label: options.label ?? \"Keycloak\",\n domain: options.domain,\n oidc: {\n issuer: realmUrl,\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n },\n ...oidcGroupsOf(options),\n };\n}\n\n/** OIDC `groups` entry for the client-option presets. */\nfunction oidcGroupsOf(options: SSOClientOptions): Pick<ShelfSSOProvider, \"groups\"> {\n const groups = groupSyncOf({ claim: options.groupsClaim, admins: options.adminGroups });\n return groups === undefined ? {} : { groups };\n}\n\n/** Okta SSO (custom authorization server, OIDC discovery). */\nexport function ssoOktaPreset(\n options: SSOClientOptions & { authorizationServerId: string },\n): ShelfSSOProvider {\n const issuer = `https://${options.domain}/oauth2/${options.authorizationServerId}`;\n return {\n id: options.id ?? \"okta\",\n label: options.label ?? \"Okta\",\n domain: options.domain,\n oidc: { issuer, clientId: options.clientId, clientSecret: options.clientSecret },\n ...oidcGroupsOf(options),\n };\n}\n\n/** Auth0 SSO (OIDC discovery over the tenant domain). */\nexport function ssoAuth0Preset(options: SSOClientOptions & { tenant: string }): ShelfSSOProvider {\n return {\n id: options.id ?? \"auth0\",\n label: options.label ?? \"Auth0\",\n domain: options.domain,\n oidc: {\n issuer: `https://${options.tenant}/`,\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n },\n ...oidcGroupsOf(options),\n };\n}\n\n/** Google Workspace SSO (OIDC discovery; `domain` is the Workspace domain). */\nexport function ssoGooglePreset(options: SSOClientOptions): ShelfSSOProvider {\n return {\n id: options.id ?? \"google-workspace\",\n label: options.label ?? \"Google\",\n domain: options.domain,\n oidc: {\n issuer: \"https://accounts.google.com\",\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n },\n ...oidcGroupsOf(options),\n };\n}\n\n/** Microsoft Entra ID SSO (OIDC discovery over the tenant issuer). */\nexport function ssoEntraPreset(\n options: SSOClientOptions & { tenantId?: string },\n): ShelfSSOProvider {\n const tenant = options.tenantId ?? \"common\";\n return {\n id: options.id ?? \"entra\",\n label: options.label ?? \"Microsoft\",\n domain: options.domain,\n oidc: {\n issuer: `https://login.microsoftonline.com/${tenant}/v2.0`,\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n },\n ...oidcGroupsOf(options),\n };\n}\n\n/** IdP callback URLs for one SSO provider (hand these to the IdP admin). */\nexport function ssoCallbackUrls(\n baseURL: string,\n providerId: string,\n): { acs: string; metadata: string; oidcCallback: string } {\n const root = baseURL.replace(/\\/+$/u, \"\");\n return {\n acs: `${root}/api/auth/sso/saml2/sp/acs/${providerId}`,\n metadata: `${root}/api/auth/sso/saml2/sp/metadata?providerId=${providerId}`,\n oidcCallback: `${root}/api/auth/sso/callback/${providerId}`,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
4
+ //#region src/sessions.d.ts
5
+ /** One device session for the profile page. */
6
+ interface EngineSessionInfo {
7
+ id: string;
8
+ token: string;
9
+ ipAddress: string | null;
10
+ userAgent: string | null;
11
+ createdAt: string;
12
+ expiresAt: string;
13
+ }
14
+ /**
15
+ * Raw session token from a possibly-signed cookie value (`token.signature`).
16
+ * Single shared parser: cookies, CSRF binding, and table lookups must agree.
17
+ */
18
+ declare function unsignedToken(value: string): string;
19
+ /** Active sessions for a user, newest first (expired rows are dropped). */
20
+ declare function listUserSessions(db: DatabaseAdapter, userId: string): Promise<EngineSessionInfo[]>;
21
+ //#endregion
22
+ export { EngineSessionInfo, listUserSessions, unsignedToken };
23
+ //# sourceMappingURL=sessions.d.mts.map
@@ -0,0 +1,51 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { baseAuthTables } from "./auth-tables.mjs";
5
+ import { tableColumn } from "./invites.mjs";
6
+ import { eq } from "drizzle-orm";
7
+ //#region src/sessions.ts
8
+ /**
9
+ * Raw session token from a possibly-signed cookie value (`token.signature`).
10
+ * Single shared parser: cookies, CSRF binding, and table lookups must agree.
11
+ */
12
+ function unsignedToken(value) {
13
+ return value.split(".")[0] ?? "";
14
+ }
15
+ /** Delete every session row for a user (eager revocation on disable). */
16
+ async function deleteUserSessions(db, userId) {
17
+ const rows = await db.list(baseAuthTables.session, { where: eq(tableColumn(baseAuthTables.session, "userId"), userId) });
18
+ await Promise.all(rows.map(async (row) => {
19
+ await db.remove(baseAuthTables.session, String(row["id"]));
20
+ }));
21
+ return rows.length;
22
+ }
23
+ /** Delete one session by token (accepts the signed cookie value or raw token). */
24
+ async function deleteSessionByToken(db, token) {
25
+ const raw = token.split(".")[0] ?? "";
26
+ if (!raw) return false;
27
+ const row = (await db.list(baseAuthTables.session, {
28
+ where: eq(tableColumn(baseAuthTables.session, "token"), raw),
29
+ limit: 1
30
+ }))[0];
31
+ if (!row) return false;
32
+ await db.remove(baseAuthTables.session, String(row["id"]));
33
+ return true;
34
+ }
35
+ /** Active sessions for a user, newest first (expired rows are dropped). */
36
+ async function listUserSessions(db, userId) {
37
+ const rows = await db.list(baseAuthTables.session, { where: eq(tableColumn(baseAuthTables.session, "userId"), userId) });
38
+ const now = Date.now();
39
+ return rows.filter((row) => new Date(String(row["expiresAt"])).getTime() > now).map((row) => ({
40
+ id: String(row["id"]),
41
+ token: String(row["token"]),
42
+ ipAddress: row["ipAddress"] ?? null,
43
+ userAgent: row["userAgent"] ?? null,
44
+ createdAt: String(row["createdAt"]),
45
+ expiresAt: String(row["expiresAt"])
46
+ })).toSorted((a, b) => a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt < b.createdAt ? 1 : -1);
47
+ }
48
+ //#endregion
49
+ export { deleteSessionByToken, deleteUserSessions, listUserSessions, unsignedToken };
50
+
51
+ //# sourceMappingURL=sessions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.mjs","names":[],"sources":["../src/sessions.ts"],"sourcesContent":["/**\n * Shelf-side session inventory for the profile device list.\n *\n * Listing reads our `session` table directly (full rows, including tokens\n * for revoke + current-device matching). Mutations go through engine\n * endpoints (`/revoke-session` is ownership-checked server-side,\n * `/revoke-other-sessions` keeps the caller), never raw deletes.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { eq } from \"drizzle-orm\";\nimport { baseAuthTables } from \"./auth-tables.ts\";\nimport { tableColumn } from \"./invites.ts\";\n\n/** One device session for the profile page. */\nexport interface EngineSessionInfo {\n id: string;\n token: string;\n ipAddress: string | null;\n userAgent: string | null;\n createdAt: string;\n expiresAt: string;\n}\n\n/**\n * Raw session token from a possibly-signed cookie value (`token.signature`).\n * Single shared parser: cookies, CSRF binding, and table lookups must agree.\n */\nexport function unsignedToken(value: string): string {\n return value.split(\".\")[0] ?? \"\";\n}\n\n/** Delete every session row for a user (eager revocation on disable). */\nexport async function deleteUserSessions(db: DatabaseAdapter, userId: string): Promise<number> {\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"userId\"), userId),\n })) as Record<string, unknown>[];\n await Promise.all(\n rows.map(async (row) => {\n await db.remove(baseAuthTables.session, String(row[\"id\"]));\n }),\n );\n return rows.length;\n}\n\n/** Delete one session by token (accepts the signed cookie value or raw token). */\nexport async function deleteSessionByToken(db: DatabaseAdapter, token: string): Promise<boolean> {\n const raw = token.split(\".\")[0] ?? \"\";\n if (!raw) {\n return false;\n }\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"token\"), raw),\n limit: 1,\n })) as Record<string, unknown>[];\n const row = rows[0];\n if (!row) {\n return false;\n }\n await db.remove(baseAuthTables.session, String(row[\"id\"]));\n return true;\n}\n\n/** Active sessions for a user, newest first (expired rows are dropped). */\nexport async function listUserSessions(\n db: DatabaseAdapter,\n userId: string,\n): Promise<EngineSessionInfo[]> {\n const rows = (await db.list(baseAuthTables.session, {\n where: eq(tableColumn(baseAuthTables.session, \"userId\"), userId),\n })) as Record<string, unknown>[];\n const now = Date.now();\n return rows\n .filter((row) => new Date(String(row[\"expiresAt\"])).getTime() > now)\n .map((row) => ({\n id: String(row[\"id\"]),\n token: String(row[\"token\"]),\n ipAddress: (row[\"ipAddress\"] as string | null) ?? null,\n userAgent: (row[\"userAgent\"] as string | null) ?? null,\n createdAt: String(row[\"createdAt\"]),\n expiresAt: String(row[\"expiresAt\"]),\n }))\n .toSorted((a, b) =>\n a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt < b.createdAt ? 1 : -1,\n );\n}\n"],"mappings":";;;;;;;;;;;AA2BA,SAAgB,cAAc,OAAuB;CACnD,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;AAChC;;AAGA,eAAsB,mBAAmB,IAAqB,QAAiC;CAC7F,MAAM,OAAQ,MAAM,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC;CACD,MAAM,QAAQ,IACZ,KAAK,IAAI,OAAO,QAAQ;EACtB,MAAM,GAAG,OAAO,eAAe,SAAS,OAAO,IAAI,KAAK,CAAC;CAC3D,CAAC,CACH;CACA,OAAO,KAAK;AACd;;AAGA,eAAsB,qBAAqB,IAAqB,OAAiC;CAC/F,MAAM,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;CACnC,IAAI,CAAC,KACH,OAAO;CAMT,MAAM,OAAM,MAJQ,GAAG,KAAK,eAAe,SAAS;EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,OAAO,GAAG,GAAG;EAC3D,OAAO;CACT,CAAC,EAAA,CACgB;CACjB,IAAI,CAAC,KACH,OAAO;CAET,MAAM,GAAG,OAAO,eAAe,SAAS,OAAO,IAAI,KAAK,CAAC;CACzD,OAAO;AACT;;AAGA,eAAsB,iBACpB,IACA,QAC8B;CAC9B,MAAM,OAAQ,MAAM,GAAG,KAAK,eAAe,SAAS,EAClD,OAAO,GAAG,YAAY,eAAe,SAAS,QAAQ,GAAG,MAAM,EACjE,CAAC;CACD,MAAM,MAAM,KAAK,IAAI;CACrB,OAAO,KACJ,QAAQ,QAAQ,IAAI,KAAK,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,CACnE,KAAK,SAAS;EACb,IAAI,OAAO,IAAI,KAAK;EACpB,OAAO,OAAO,IAAI,QAAQ;EAC1B,WAAY,IAAI,gBAAkC;EAClD,WAAY,IAAI,gBAAkC;EAClD,WAAW,OAAO,IAAI,YAAY;EAClC,WAAW,OAAO,IAAI,YAAY;CACpC,EAAE,CAAC,CACF,UAAU,GAAG,MACZ,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,cAAc,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,IAAI,EAC3F;AACJ"}
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@storyshelf/auth",
3
+ "version": "0.5.3",
4
+ "description": "Better Auth engine for StoryShelf: opaque DBAdapter bridge, shelf factory, and auth presets.",
5
+ "keywords": [
6
+ "auth",
7
+ "better-auth",
8
+ "self-hosted",
9
+ "storybook"
10
+ ],
11
+ "homepage": "https://github.com/GuptaSiddhant/StoryShelf#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/GuptaSiddhant/StoryShelf/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": {
17
+ "name": "Siddhant Gupta",
18
+ "url": "https://guptasiddhant.com"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/GuptaSiddhant/StoryShelf.git",
23
+ "directory": "packages/auth"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "types": "./dist/index.d.mts",
31
+ "exports": {
32
+ ".": {
33
+ "source": "./src/index.ts",
34
+ "default": "./dist/index.mjs"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "publishConfig": {
39
+ "exports": {
40
+ ".": "./dist/index.mjs",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsdown",
47
+ "dev": "tsdown -w",
48
+ "fmt": "oxfmt ./src",
49
+ "lint": "oxlint --type-aware --type-check ./src",
50
+ "test": "vitest run",
51
+ "prepublishOnly": "nub run build"
52
+ },
53
+ "dependencies": {
54
+ "@better-auth/passkey": "1.7.6",
55
+ "@better-auth/sso": "1.7.6",
56
+ "@storyshelf/core": "0.5.3",
57
+ "better-auth": "1.7.6",
58
+ "drizzle-orm": "^0.44.0",
59
+ "zod": "^4.2.0"
60
+ },
61
+ "devDependencies": {
62
+ "@storyshelf/db-postgres": "0.5.3",
63
+ "@storyshelf/db-sqlite": "0.5.3",
64
+ "@types/node": "^26.0.0",
65
+ "@vitest/coverage-v8": "^4.0.0",
66
+ "oxfmt": "^0.65.0",
67
+ "oxlint": "^1.80.0",
68
+ "oxlint-tsgolint": "^7.0.2001",
69
+ "tsdown": "^0.22.14",
70
+ "typescript": "^7.0.2",
71
+ "vitest": "^5.0.0"
72
+ },
73
+ "jsr": {
74
+ "runtimeCompat": {
75
+ "node": true,
76
+ "deno": true,
77
+ "browser": false,
78
+ "workerd": false,
79
+ "bun": true
80
+ }
81
+ }
82
+ }