@sonicjs-cms/core 2.10.0 → 2.11.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.
Files changed (82) hide show
  1. package/dist/{chunk-CJYFSKH7.js → chunk-2MXF4RYZ.js} +3 -3
  2. package/dist/{chunk-CJYFSKH7.js.map → chunk-2MXF4RYZ.js.map} +1 -1
  3. package/dist/{chunk-MNFY6DWY.cjs → chunk-56GUBLJE.cjs} +7 -7
  4. package/dist/{chunk-MNFY6DWY.cjs.map → chunk-56GUBLJE.cjs.map} +1 -1
  5. package/dist/{chunk-IIBRG5S5.cjs → chunk-6BVLPACH.cjs} +408 -2
  6. package/dist/chunk-6BVLPACH.cjs.map +1 -0
  7. package/dist/{chunk-RCA6R6VE.cjs → chunk-ASAEJ4B7.cjs} +315 -162
  8. package/dist/chunk-ASAEJ4B7.cjs.map +1 -0
  9. package/dist/{chunk-IT2TC4ZD.cjs → chunk-B2ASV5RD.cjs} +13 -7
  10. package/dist/chunk-B2ASV5RD.cjs.map +1 -0
  11. package/dist/{chunk-IZWNIUJI.js → chunk-BUU2US2Z.js} +3 -3
  12. package/dist/{chunk-IZWNIUJI.js.map → chunk-BUU2US2Z.js.map} +1 -1
  13. package/dist/{chunk-ZMVWMJ3S.cjs → chunk-DE5YTNCD.cjs} +9 -2
  14. package/dist/chunk-DE5YTNCD.cjs.map +1 -0
  15. package/dist/{chunk-4TTMQQC7.js → chunk-GKRGDJGG.js} +10 -4
  16. package/dist/chunk-GKRGDJGG.js.map +1 -0
  17. package/dist/{chunk-6O3RJV3C.js → chunk-H55AYIRI.js} +9 -2
  18. package/dist/chunk-H55AYIRI.js.map +1 -0
  19. package/dist/{chunk-JTNUM7JE.js → chunk-JTQBNSZX.js} +187 -34
  20. package/dist/chunk-JTQBNSZX.js.map +1 -0
  21. package/dist/{chunk-64APW3DW.cjs → chunk-LFAQUR7P.cjs} +9 -2
  22. package/dist/chunk-LFAQUR7P.cjs.map +1 -0
  23. package/dist/{chunk-27AOVQTR.js → chunk-NMLFKXWW.js} +402 -3
  24. package/dist/chunk-NMLFKXWW.js.map +1 -0
  25. package/dist/{chunk-EKPLKUZT.cjs → chunk-QLPFENZ2.cjs} +3 -3
  26. package/dist/{chunk-EKPLKUZT.cjs.map → chunk-QLPFENZ2.cjs.map} +1 -1
  27. package/dist/{chunk-KYGRJCZM.cjs → chunk-QTFKZBLC.cjs} +3 -2
  28. package/dist/chunk-QTFKZBLC.cjs.map +1 -0
  29. package/dist/{chunk-LOUJRBXV.js → chunk-QXOZI5Q2.js} +3 -2
  30. package/dist/chunk-QXOZI5Q2.js.map +1 -0
  31. package/dist/{chunk-7JMMLHPQ.js → chunk-VJCLJH3X.js} +9 -2
  32. package/dist/chunk-VJCLJH3X.js.map +1 -0
  33. package/dist/index.cjs +751 -152
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +125 -5
  36. package/dist/index.d.ts +125 -5
  37. package/dist/index.js +582 -15
  38. package/dist/index.js.map +1 -1
  39. package/dist/middleware.cjs +29 -29
  40. package/dist/middleware.js +3 -3
  41. package/dist/migrations-UFVJTPVT.js +4 -0
  42. package/dist/{migrations-N2C2VPJU.js.map → migrations-UFVJTPVT.js.map} +1 -1
  43. package/dist/migrations-VNYOSUNE.cjs +13 -0
  44. package/dist/{migrations-ONIAY6GK.cjs.map → migrations-VNYOSUNE.cjs.map} +1 -1
  45. package/dist/{plugin-0Xogrln-.d.cts → plugin-DDYetMF-.d.cts} +1 -0
  46. package/dist/{plugin-0Xogrln-.d.ts → plugin-DDYetMF-.d.ts} +1 -0
  47. package/dist/{plugin-bootstrap-fpG98Otb.d.cts → plugin-bootstrap-DCXpeQVb.d.cts} +229 -1
  48. package/dist/{plugin-bootstrap-WmpvYM5w.d.ts → plugin-bootstrap-DXBAYaqM.d.ts} +229 -1
  49. package/dist/{plugin-manager-GcIeb226.d.cts → plugin-manager-BoM3Q7o7.d.cts} +1 -1
  50. package/dist/{plugin-manager-Clf2gXwj.d.ts → plugin-manager-Efx9RyDX.d.ts} +1 -1
  51. package/dist/plugins.cjs +10 -10
  52. package/dist/plugins.d.cts +2 -2
  53. package/dist/plugins.d.ts +2 -2
  54. package/dist/plugins.js +2 -2
  55. package/dist/routes.cjs +29 -29
  56. package/dist/routes.js +6 -6
  57. package/dist/services.cjs +60 -32
  58. package/dist/services.d.cts +1 -1
  59. package/dist/services.d.ts +1 -1
  60. package/dist/services.js +3 -3
  61. package/dist/types.cjs +2 -2
  62. package/dist/types.d.cts +1 -1
  63. package/dist/types.d.ts +1 -1
  64. package/dist/types.js +1 -1
  65. package/dist/utils.cjs +11 -11
  66. package/dist/utils.js +1 -1
  67. package/migrations/033_form_content_integration.sql +19 -0
  68. package/package.json +1 -1
  69. package/dist/chunk-27AOVQTR.js.map +0 -1
  70. package/dist/chunk-4TTMQQC7.js.map +0 -1
  71. package/dist/chunk-64APW3DW.cjs.map +0 -1
  72. package/dist/chunk-6O3RJV3C.js.map +0 -1
  73. package/dist/chunk-7JMMLHPQ.js.map +0 -1
  74. package/dist/chunk-IIBRG5S5.cjs.map +0 -1
  75. package/dist/chunk-IT2TC4ZD.cjs.map +0 -1
  76. package/dist/chunk-JTNUM7JE.js.map +0 -1
  77. package/dist/chunk-KYGRJCZM.cjs.map +0 -1
  78. package/dist/chunk-LOUJRBXV.js.map +0 -1
  79. package/dist/chunk-RCA6R6VE.cjs.map +0 -1
  80. package/dist/chunk-ZMVWMJ3S.cjs.map +0 -1
  81. package/dist/migrations-N2C2VPJU.js +0 -4
  82. package/dist/migrations-ONIAY6GK.cjs +0 -13
package/dist/index.js CHANGED
@@ -1,26 +1,26 @@
1
- import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-JTNUM7JE.js';
2
- export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes } from './chunk-JTNUM7JE.js';
3
- import { SettingsService, setAppInstance, schema_exports } from './chunk-7JMMLHPQ.js';
4
- export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-7JMMLHPQ.js';
5
- import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-4TTMQQC7.js';
6
- export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-4TTMQQC7.js';
7
- export { PluginBootstrapService, PluginService as PluginServiceClass, cleanupRemovedCollections, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, registerCollections, syncCollection, syncCollections, validateCollectionConfig } from './chunk-27AOVQTR.js';
8
- export { MigrationService } from './chunk-6O3RJV3C.js';
1
+ import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-JTQBNSZX.js';
2
+ export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes } from './chunk-JTQBNSZX.js';
3
+ import { SettingsService, setAppInstance, schema_exports } from './chunk-VJCLJH3X.js';
4
+ export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-VJCLJH3X.js';
5
+ import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-GKRGDJGG.js';
6
+ export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-GKRGDJGG.js';
7
+ export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-NMLFKXWW.js';
8
+ export { MigrationService } from './chunk-H55AYIRI.js';
9
9
  export { renderFilterBar } from './chunk-74XCYEI7.js';
10
10
  import { init_admin_layout_catalyst_template, renderAdminLayout, renderAdminLayoutCatalyst } from './chunk-JJS7JZCH.js';
11
11
  export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-JJS7JZCH.js';
12
- export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-CJYFSKH7.js';
12
+ export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-2MXF4RYZ.js';
13
13
  import { PluginBuilder } from './chunk-J5WGMRSU.js';
14
14
  export { PluginBuilder, PluginHelpers } from './chunk-J5WGMRSU.js';
15
- import { package_default, getCoreVersion } from './chunk-IZWNIUJI.js';
16
- export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-IZWNIUJI.js';
15
+ import { package_default, getCoreVersion } from './chunk-BUU2US2Z.js';
16
+ export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-BUU2US2Z.js';
17
17
  import './chunk-X7ZAEI5S.js';
18
18
  export { metricsTracker } from './chunk-FICTAGD4.js';
19
19
  export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
20
- export { HOOKS } from './chunk-LOUJRBXV.js';
20
+ export { HOOKS } from './chunk-QXOZI5Q2.js';
21
21
  import './chunk-V4OQ3NZ2.js';
22
22
  import { Hono } from 'hono';
23
- import { setCookie } from 'hono/cookie';
23
+ import { setCookie, getCookie } from 'hono/cookie';
24
24
  import { z } from 'zod';
25
25
  import { drizzle } from 'drizzle-orm/d1';
26
26
 
@@ -1826,7 +1826,8 @@ function createOTPLoginPlugin() {
1826
1826
  email: normalizedEmail,
1827
1827
  ipAddress,
1828
1828
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1829
- appName: siteName
1829
+ appName: siteName,
1830
+ logoUrl: settings.logoUrl || ""
1830
1831
  });
1831
1832
  const emailPlugin2 = await db.prepare(`
1832
1833
  SELECT settings FROM plugins WHERE id = 'email'
@@ -2001,6 +2002,567 @@ function createOTPLoginPlugin() {
2001
2002
  }
2002
2003
  var otpLoginPlugin = createOTPLoginPlugin();
2003
2004
 
2005
+ // src/plugins/core-plugins/oauth-providers/oauth-service.ts
2006
+ var GITHUB_PROVIDER = {
2007
+ id: "github",
2008
+ name: "GitHub",
2009
+ authorizeUrl: "https://github.com/login/oauth/authorize",
2010
+ tokenUrl: "https://github.com/login/oauth/access_token",
2011
+ userInfoUrl: "https://api.github.com/user",
2012
+ scopes: ["read:user", "user:email"],
2013
+ mapProfile: (profile) => ({
2014
+ providerAccountId: String(profile.id),
2015
+ email: profile.email || "",
2016
+ name: profile.name || profile.login || "",
2017
+ avatar: profile.avatar_url || void 0
2018
+ })
2019
+ };
2020
+ var GOOGLE_PROVIDER = {
2021
+ id: "google",
2022
+ name: "Google",
2023
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
2024
+ tokenUrl: "https://oauth2.googleapis.com/token",
2025
+ userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
2026
+ scopes: ["openid", "email", "profile"],
2027
+ mapProfile: (profile) => ({
2028
+ providerAccountId: String(profile.id),
2029
+ email: profile.email || "",
2030
+ name: profile.name || "",
2031
+ avatar: profile.picture || void 0
2032
+ })
2033
+ };
2034
+ var BUILT_IN_PROVIDERS = {
2035
+ github: GITHUB_PROVIDER,
2036
+ google: GOOGLE_PROVIDER
2037
+ };
2038
+ var OAuthService = class {
2039
+ constructor(db) {
2040
+ this.db = db;
2041
+ }
2042
+ /**
2043
+ * Build the authorization redirect URL for a provider.
2044
+ */
2045
+ buildAuthorizeUrl(provider, clientId, redirectUri, state) {
2046
+ const params = new URLSearchParams({
2047
+ client_id: clientId,
2048
+ redirect_uri: redirectUri,
2049
+ response_type: "code",
2050
+ scope: provider.scopes.join(" "),
2051
+ state
2052
+ });
2053
+ if (provider.id === "google") {
2054
+ params.set("access_type", "offline");
2055
+ params.set("prompt", "consent");
2056
+ }
2057
+ return `${provider.authorizeUrl}?${params.toString()}`;
2058
+ }
2059
+ /**
2060
+ * Exchange authorization code for tokens using native fetch.
2061
+ */
2062
+ async exchangeCode(provider, clientId, clientSecret, code, redirectUri) {
2063
+ const body = {
2064
+ client_id: clientId,
2065
+ client_secret: clientSecret,
2066
+ code,
2067
+ redirect_uri: redirectUri,
2068
+ grant_type: "authorization_code"
2069
+ };
2070
+ const response = await fetch(provider.tokenUrl, {
2071
+ method: "POST",
2072
+ headers: {
2073
+ "Content-Type": "application/x-www-form-urlencoded",
2074
+ "Accept": "application/json"
2075
+ },
2076
+ body: new URLSearchParams(body).toString()
2077
+ });
2078
+ if (!response.ok) {
2079
+ const errorText = await response.text();
2080
+ throw new Error(`Token exchange failed (${response.status}): ${errorText}`);
2081
+ }
2082
+ const data = await response.json();
2083
+ if (data.error) {
2084
+ throw new Error(`Token exchange error: ${data.error_description || data.error}`);
2085
+ }
2086
+ return {
2087
+ access_token: data.access_token,
2088
+ refresh_token: data.refresh_token,
2089
+ expires_in: data.expires_in ? Number(data.expires_in) : void 0
2090
+ };
2091
+ }
2092
+ /**
2093
+ * Fetch user profile from the provider's userinfo endpoint.
2094
+ */
2095
+ async fetchUserProfile(provider, accessToken) {
2096
+ const headers = {
2097
+ "Authorization": `Bearer ${accessToken}`,
2098
+ "Accept": "application/json"
2099
+ };
2100
+ if (provider.id === "github") {
2101
+ headers["Authorization"] = `token ${accessToken}`;
2102
+ }
2103
+ const response = await fetch(provider.userInfoUrl, { headers });
2104
+ if (!response.ok) {
2105
+ throw new Error(`Failed to fetch user profile (${response.status})`);
2106
+ }
2107
+ const profile = await response.json();
2108
+ if (provider.id === "github" && !profile.email) {
2109
+ const emailResponse = await fetch("https://api.github.com/user/emails", {
2110
+ headers: {
2111
+ "Authorization": `token ${accessToken}`,
2112
+ "Accept": "application/json"
2113
+ }
2114
+ });
2115
+ if (emailResponse.ok) {
2116
+ const emails = await emailResponse.json();
2117
+ const primaryEmail = emails.find((e) => e.primary && e.verified);
2118
+ if (primaryEmail) {
2119
+ profile.email = primaryEmail.email;
2120
+ }
2121
+ }
2122
+ }
2123
+ return provider.mapProfile(profile);
2124
+ }
2125
+ // ─── Database Operations ────────────────────────────────────────────────
2126
+ /**
2127
+ * Find an existing OAuth account link.
2128
+ */
2129
+ async findOAuthAccount(provider, providerAccountId) {
2130
+ return await this.db.prepare(`
2131
+ SELECT * FROM oauth_accounts
2132
+ WHERE provider = ? AND provider_account_id = ?
2133
+ `).bind(provider, providerAccountId).first();
2134
+ }
2135
+ /**
2136
+ * Find all OAuth accounts for a user.
2137
+ */
2138
+ async findUserOAuthAccounts(userId) {
2139
+ const result = await this.db.prepare(`
2140
+ SELECT * FROM oauth_accounts WHERE user_id = ?
2141
+ `).bind(userId).all();
2142
+ return result.results || [];
2143
+ }
2144
+ /**
2145
+ * Create a new OAuth account link.
2146
+ */
2147
+ async createOAuthAccount(params) {
2148
+ const id = crypto.randomUUID();
2149
+ const now = Date.now();
2150
+ await this.db.prepare(`
2151
+ INSERT INTO oauth_accounts (
2152
+ id, user_id, provider, provider_account_id,
2153
+ access_token, refresh_token, token_expires_at,
2154
+ profile_data, created_at, updated_at
2155
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2156
+ `).bind(
2157
+ id,
2158
+ params.userId,
2159
+ params.provider,
2160
+ params.providerAccountId,
2161
+ params.accessToken,
2162
+ params.refreshToken || null,
2163
+ params.tokenExpiresAt || null,
2164
+ params.profileData || null,
2165
+ now,
2166
+ now
2167
+ ).run();
2168
+ return {
2169
+ id,
2170
+ user_id: params.userId,
2171
+ provider: params.provider,
2172
+ provider_account_id: params.providerAccountId,
2173
+ access_token: params.accessToken,
2174
+ refresh_token: params.refreshToken || null,
2175
+ token_expires_at: params.tokenExpiresAt || null,
2176
+ profile_data: params.profileData || null,
2177
+ created_at: now,
2178
+ updated_at: now
2179
+ };
2180
+ }
2181
+ /**
2182
+ * Update tokens for an existing OAuth account.
2183
+ */
2184
+ async updateOAuthTokens(id, accessToken, refreshToken, tokenExpiresAt) {
2185
+ await this.db.prepare(`
2186
+ UPDATE oauth_accounts
2187
+ SET access_token = ?, refresh_token = ?, token_expires_at = ?, updated_at = ?
2188
+ WHERE id = ?
2189
+ `).bind(accessToken, refreshToken || null, tokenExpiresAt || null, Date.now(), id).run();
2190
+ }
2191
+ /**
2192
+ * Unlink an OAuth account from a user (only if they have another auth method).
2193
+ */
2194
+ async unlinkOAuthAccount(userId, provider) {
2195
+ const user = await this.db.prepare(`
2196
+ SELECT password_hash FROM users WHERE id = ?
2197
+ `).bind(userId).first();
2198
+ const otherLinks = await this.db.prepare(`
2199
+ SELECT COUNT(*) as count FROM oauth_accounts
2200
+ WHERE user_id = ? AND provider != ?
2201
+ `).bind(userId, provider).first();
2202
+ const hasPassword = !!user?.password_hash;
2203
+ const hasOtherLinks = (otherLinks?.count || 0) > 0;
2204
+ if (!hasPassword && !hasOtherLinks) {
2205
+ return false;
2206
+ }
2207
+ await this.db.prepare(`
2208
+ DELETE FROM oauth_accounts WHERE user_id = ? AND provider = ?
2209
+ `).bind(userId, provider).run();
2210
+ return true;
2211
+ }
2212
+ /**
2213
+ * Find a user by email.
2214
+ */
2215
+ async findUserByEmail(email) {
2216
+ return await this.db.prepare(`
2217
+ SELECT id, email, role, is_active, first_name, last_name
2218
+ FROM users WHERE email = ?
2219
+ `).bind(email.toLowerCase()).first();
2220
+ }
2221
+ /**
2222
+ * Create a new user from an OAuth profile.
2223
+ */
2224
+ async createUserFromOAuth(profile) {
2225
+ const id = crypto.randomUUID();
2226
+ const now = Date.now();
2227
+ const email = profile.email.toLowerCase();
2228
+ const nameParts = (profile.name || email.split("@")[0] || "User").split(" ");
2229
+ const firstName = nameParts[0] || "User";
2230
+ const lastName = nameParts.slice(1).join(" ") || "";
2231
+ const username = email.split("@")[0] || id.substring(0, 8);
2232
+ const existing = await this.db.prepare(
2233
+ "SELECT id FROM users WHERE username = ?"
2234
+ ).bind(username).first();
2235
+ const finalUsername = existing ? `${username}-${id.substring(0, 6)}` : username;
2236
+ await this.db.prepare(`
2237
+ INSERT INTO users (
2238
+ id, email, username, first_name, last_name,
2239
+ password_hash, role, avatar, is_active, created_at, updated_at
2240
+ ) VALUES (?, ?, ?, ?, ?, NULL, 'viewer', ?, 1, ?, ?)
2241
+ `).bind(
2242
+ id,
2243
+ email,
2244
+ finalUsername,
2245
+ firstName,
2246
+ lastName,
2247
+ profile.avatar || null,
2248
+ now,
2249
+ now
2250
+ ).run();
2251
+ return id;
2252
+ }
2253
+ /**
2254
+ * Generate a cryptographically random state parameter for CSRF protection.
2255
+ */
2256
+ generateState() {
2257
+ const bytes = new Uint8Array(32);
2258
+ crypto.getRandomValues(bytes);
2259
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2260
+ }
2261
+ };
2262
+
2263
+ // src/plugins/core-plugins/oauth-providers/index.ts
2264
+ var STATE_COOKIE_NAME = "oauth_state";
2265
+ var STATE_COOKIE_MAX_AGE = 600;
2266
+ function createOAuthProvidersPlugin() {
2267
+ const builder = PluginBuilder.create({
2268
+ name: "oauth-providers",
2269
+ version: "1.0.0-beta.1",
2270
+ description: "OAuth2/OIDC social login with GitHub, Google, and more"
2271
+ });
2272
+ builder.metadata({
2273
+ author: {
2274
+ name: "SonicJS Team",
2275
+ email: "team@sonicjs.com"
2276
+ },
2277
+ license: "MIT",
2278
+ compatibility: "^2.0.0"
2279
+ });
2280
+ function getCallbackUrl(c, provider) {
2281
+ const proto = c.req.header("x-forwarded-proto") || "https";
2282
+ const host = c.req.header("host") || "localhost";
2283
+ return `${proto}://${host}/auth/oauth/${provider}/callback`;
2284
+ }
2285
+ async function loadSettings(db) {
2286
+ const row = await db.prepare(
2287
+ `SELECT settings FROM plugins WHERE id = 'oauth-providers'`
2288
+ ).first();
2289
+ if (!row?.settings) return null;
2290
+ try {
2291
+ return JSON.parse(row.settings);
2292
+ } catch {
2293
+ return null;
2294
+ }
2295
+ }
2296
+ function getProviderCredentials(settings, providerId) {
2297
+ if (!settings?.providers?.[providerId]) return null;
2298
+ const p = settings.providers[providerId];
2299
+ if (!p.enabled || !p.clientId || !p.clientSecret) return null;
2300
+ return { clientId: p.clientId, clientSecret: p.clientSecret };
2301
+ }
2302
+ const oauthAPI = new Hono();
2303
+ oauthAPI.get("/:provider", async (c) => {
2304
+ try {
2305
+ const providerId = c.req.param("provider");
2306
+ const providerConfig = BUILT_IN_PROVIDERS[providerId];
2307
+ if (!providerConfig) {
2308
+ return c.json({ error: `Unknown OAuth provider: ${providerId}` }, 400);
2309
+ }
2310
+ const db = c.env.DB;
2311
+ const settings = await loadSettings(db);
2312
+ const creds = getProviderCredentials(settings, providerId);
2313
+ if (!creds) {
2314
+ return c.json({
2315
+ error: `OAuth provider "${providerId}" is not configured or not enabled`
2316
+ }, 400);
2317
+ }
2318
+ const oauthService = new OAuthService(db);
2319
+ const state = oauthService.generateState();
2320
+ const redirectUri = getCallbackUrl(c, providerId);
2321
+ setCookie(c, STATE_COOKIE_NAME, state, {
2322
+ httpOnly: true,
2323
+ secure: true,
2324
+ sameSite: "Lax",
2325
+ // Lax required for OAuth redirect flow
2326
+ maxAge: STATE_COOKIE_MAX_AGE,
2327
+ path: "/auth/oauth"
2328
+ });
2329
+ const authorizeUrl = oauthService.buildAuthorizeUrl(
2330
+ providerConfig,
2331
+ creds.clientId,
2332
+ redirectUri,
2333
+ state
2334
+ );
2335
+ return c.redirect(authorizeUrl);
2336
+ } catch (error) {
2337
+ console.error("OAuth authorize error:", error);
2338
+ return c.json({ error: "Failed to initiate OAuth flow" }, 500);
2339
+ }
2340
+ });
2341
+ oauthAPI.get("/:provider/callback", async (c) => {
2342
+ try {
2343
+ const providerId = c.req.param("provider");
2344
+ const providerConfig = BUILT_IN_PROVIDERS[providerId];
2345
+ if (!providerConfig) {
2346
+ return c.redirect("/auth/login?error=Unknown OAuth provider");
2347
+ }
2348
+ const stateParam = c.req.query("state");
2349
+ const stateCookie = getCookie(c, STATE_COOKIE_NAME);
2350
+ if (!stateParam || !stateCookie || stateParam !== stateCookie) {
2351
+ return c.redirect("/auth/login?error=Invalid OAuth state. Please try again.");
2352
+ }
2353
+ setCookie(c, STATE_COOKIE_NAME, "", {
2354
+ httpOnly: true,
2355
+ secure: true,
2356
+ sameSite: "Lax",
2357
+ maxAge: 0,
2358
+ path: "/auth/oauth"
2359
+ });
2360
+ const errorParam = c.req.query("error");
2361
+ if (errorParam) {
2362
+ const errorDesc = c.req.query("error_description") || errorParam;
2363
+ return c.redirect(`/auth/login?error=${encodeURIComponent(errorDesc)}`);
2364
+ }
2365
+ const code = c.req.query("code");
2366
+ if (!code) {
2367
+ return c.redirect("/auth/login?error=No authorization code received");
2368
+ }
2369
+ const db = c.env.DB;
2370
+ const settings = await loadSettings(db);
2371
+ const creds = getProviderCredentials(settings, providerId);
2372
+ if (!creds) {
2373
+ return c.redirect("/auth/login?error=OAuth provider not configured");
2374
+ }
2375
+ const oauthService = new OAuthService(db);
2376
+ const redirectUri = getCallbackUrl(c, providerId);
2377
+ const tokens = await oauthService.exchangeCode(
2378
+ providerConfig,
2379
+ creds.clientId,
2380
+ creds.clientSecret,
2381
+ code,
2382
+ redirectUri
2383
+ );
2384
+ const profile = await oauthService.fetchUserProfile(providerConfig, tokens.access_token);
2385
+ if (!profile.email) {
2386
+ return c.redirect("/auth/login?error=Could not retrieve email from OAuth provider. Please ensure your email is public or grant email permission.");
2387
+ }
2388
+ const tokenExpiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : null;
2389
+ const existingOAuth = await oauthService.findOAuthAccount(providerId, profile.providerAccountId);
2390
+ if (existingOAuth) {
2391
+ await oauthService.updateOAuthTokens(
2392
+ existingOAuth.id,
2393
+ tokens.access_token,
2394
+ tokens.refresh_token,
2395
+ tokenExpiresAt ?? void 0
2396
+ );
2397
+ const user = await db.prepare(
2398
+ "SELECT id, email, role, is_active FROM users WHERE id = ?"
2399
+ ).bind(existingOAuth.user_id).first();
2400
+ if (!user || !user.is_active) {
2401
+ return c.redirect("/auth/login?error=Account is deactivated");
2402
+ }
2403
+ const jwt2 = await AuthManager.generateToken(
2404
+ user.id,
2405
+ user.email,
2406
+ user.role,
2407
+ c.env.JWT_SECRET
2408
+ );
2409
+ AuthManager.setAuthCookie(c, jwt2, { sameSite: "Lax" });
2410
+ return c.redirect("/admin");
2411
+ }
2412
+ const existingUser = await oauthService.findUserByEmail(profile.email);
2413
+ if (existingUser) {
2414
+ if (!existingUser.is_active) {
2415
+ return c.redirect("/auth/login?error=Account is deactivated");
2416
+ }
2417
+ await oauthService.createOAuthAccount({
2418
+ userId: existingUser.id,
2419
+ provider: providerId,
2420
+ providerAccountId: profile.providerAccountId,
2421
+ accessToken: tokens.access_token,
2422
+ refreshToken: tokens.refresh_token,
2423
+ tokenExpiresAt: tokenExpiresAt ?? void 0,
2424
+ profileData: JSON.stringify(profile)
2425
+ });
2426
+ const jwt2 = await AuthManager.generateToken(
2427
+ existingUser.id,
2428
+ existingUser.email,
2429
+ existingUser.role,
2430
+ c.env.JWT_SECRET
2431
+ );
2432
+ AuthManager.setAuthCookie(c, jwt2, { sameSite: "Lax" });
2433
+ return c.redirect("/admin");
2434
+ }
2435
+ const newUserId = await oauthService.createUserFromOAuth(profile);
2436
+ await oauthService.createOAuthAccount({
2437
+ userId: newUserId,
2438
+ provider: providerId,
2439
+ providerAccountId: profile.providerAccountId,
2440
+ accessToken: tokens.access_token,
2441
+ refreshToken: tokens.refresh_token,
2442
+ tokenExpiresAt: tokenExpiresAt ?? void 0,
2443
+ profileData: JSON.stringify(profile)
2444
+ });
2445
+ const jwt = await AuthManager.generateToken(
2446
+ newUserId,
2447
+ profile.email.toLowerCase(),
2448
+ "viewer",
2449
+ c.env.JWT_SECRET
2450
+ );
2451
+ AuthManager.setAuthCookie(c, jwt, { sameSite: "Lax" });
2452
+ return c.redirect("/admin");
2453
+ } catch (error) {
2454
+ console.error("OAuth callback error:", error);
2455
+ const message = error instanceof Error ? error.message : "OAuth authentication failed";
2456
+ return c.redirect(`/auth/login?error=${encodeURIComponent(message)}`);
2457
+ }
2458
+ });
2459
+ oauthAPI.post("/link", async (c) => {
2460
+ try {
2461
+ const user = c.get("user");
2462
+ if (!user) {
2463
+ return c.json({ error: "Authentication required" }, 401);
2464
+ }
2465
+ const body = await c.req.json();
2466
+ const { provider } = body;
2467
+ if (!provider || !BUILT_IN_PROVIDERS[provider]) {
2468
+ return c.json({ error: "Invalid provider" }, 400);
2469
+ }
2470
+ const db = c.env.DB;
2471
+ const settings = await loadSettings(db);
2472
+ const creds = getProviderCredentials(settings, provider);
2473
+ if (!creds) {
2474
+ return c.json({ error: `OAuth provider "${provider}" is not configured` }, 400);
2475
+ }
2476
+ const oauthService = new OAuthService(db);
2477
+ const state = oauthService.generateState();
2478
+ const redirectUri = getCallbackUrl(c, provider);
2479
+ setCookie(c, STATE_COOKIE_NAME, state, {
2480
+ httpOnly: true,
2481
+ secure: true,
2482
+ sameSite: "Lax",
2483
+ maxAge: STATE_COOKIE_MAX_AGE,
2484
+ path: "/auth/oauth"
2485
+ });
2486
+ const authorizeUrl = oauthService.buildAuthorizeUrl(
2487
+ BUILT_IN_PROVIDERS[provider],
2488
+ creds.clientId,
2489
+ redirectUri,
2490
+ state
2491
+ );
2492
+ return c.json({ redirectUrl: authorizeUrl });
2493
+ } catch (error) {
2494
+ console.error("OAuth link error:", error);
2495
+ return c.json({ error: "Failed to initiate account linking" }, 500);
2496
+ }
2497
+ });
2498
+ oauthAPI.post("/unlink", async (c) => {
2499
+ try {
2500
+ const user = c.get("user");
2501
+ if (!user) {
2502
+ return c.json({ error: "Authentication required" }, 401);
2503
+ }
2504
+ const body = await c.req.json();
2505
+ const { provider } = body;
2506
+ if (!provider) {
2507
+ return c.json({ error: "Provider is required" }, 400);
2508
+ }
2509
+ const db = c.env.DB;
2510
+ const oauthService = new OAuthService(db);
2511
+ const success = await oauthService.unlinkOAuthAccount(user.userId, provider);
2512
+ if (!success) {
2513
+ return c.json({
2514
+ error: "Cannot unlink the only authentication method. Set a password first."
2515
+ }, 400);
2516
+ }
2517
+ return c.json({ success: true, message: `${provider} account unlinked` });
2518
+ } catch (error) {
2519
+ console.error("OAuth unlink error:", error);
2520
+ return c.json({ error: "Failed to unlink account" }, 500);
2521
+ }
2522
+ });
2523
+ oauthAPI.get("/accounts", async (c) => {
2524
+ try {
2525
+ const user = c.get("user");
2526
+ if (!user) {
2527
+ return c.json({ error: "Authentication required" }, 401);
2528
+ }
2529
+ const db = c.env.DB;
2530
+ const oauthService = new OAuthService(db);
2531
+ const accounts = await oauthService.findUserOAuthAccounts(user.userId);
2532
+ return c.json({
2533
+ accounts: accounts.map((a) => ({
2534
+ provider: a.provider,
2535
+ providerAccountId: a.provider_account_id,
2536
+ linkedAt: a.created_at
2537
+ }))
2538
+ });
2539
+ } catch (error) {
2540
+ console.error("OAuth accounts error:", error);
2541
+ return c.json({ error: "Failed to fetch linked accounts" }, 500);
2542
+ }
2543
+ });
2544
+ builder.addRoute("/auth/oauth", oauthAPI, {
2545
+ description: "OAuth2 social login endpoints",
2546
+ requiresAuth: false,
2547
+ priority: 100
2548
+ });
2549
+ builder.addMenuItem("OAuth Providers", "/admin/plugins/oauth-providers", {
2550
+ icon: "shield",
2551
+ order: 86,
2552
+ permissions: ["oauth:manage"]
2553
+ });
2554
+ builder.lifecycle({
2555
+ activate: async () => {
2556
+ console.info("\u2705 OAuth Providers plugin activated");
2557
+ },
2558
+ deactivate: async () => {
2559
+ console.info("\u274C OAuth Providers plugin deactivated");
2560
+ }
2561
+ });
2562
+ return builder.build();
2563
+ }
2564
+ var oauthProvidersPlugin = createOAuthProvidersPlugin();
2565
+
2004
2566
  // src/plugins/core-plugins/ai-search-plugin/services/embedding.service.ts
2005
2567
  var EmbeddingService = class {
2006
2568
  constructor(ai) {
@@ -6024,6 +6586,11 @@ function createSonicJSApp(config = {}) {
6024
6586
  }
6025
6587
  }
6026
6588
  app2.route("/admin/cache", cache_default.getRoutes());
6589
+ if (oauthProvidersPlugin.routes && oauthProvidersPlugin.routes.length > 0) {
6590
+ for (const route of oauthProvidersPlugin.routes) {
6591
+ app2.route(route.path, route.handler);
6592
+ }
6593
+ }
6027
6594
  if (otpLoginPlugin.routes && otpLoginPlugin.routes.length > 0) {
6028
6595
  for (const route of otpLoginPlugin.routes) {
6029
6596
  app2.route(route.path, route.handler);
@@ -6119,6 +6686,6 @@ function createDb(d1) {
6119
6686
  // src/index.ts
6120
6687
  var VERSION = package_default.version;
6121
6688
 
6122
- export { VERSION, createDb, createSonicJSApp, setupCoreMiddleware, setupCoreRoutes };
6689
+ export { BUILT_IN_PROVIDERS, OAuthService, VERSION, createDb, createOAuthProvidersPlugin, createSonicJSApp, oauthProvidersPlugin, setupCoreMiddleware, setupCoreRoutes };
6123
6690
  //# sourceMappingURL=index.js.map
6124
6691
  //# sourceMappingURL=index.js.map