wiki-viewer 1.4.2 → 1.4.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 (158) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -27
  4. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  5. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/agent/activity/route.js +5 -5
  24. package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js +6 -6
  26. package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js +6 -6
  28. package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js +6 -6
  30. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/deny/route.js +4 -4
  32. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/deny/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/agent/admin/registrations/route.js +4 -4
  34. package/.next/standalone/.next/server/app/api/agent/admin/registrations/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js +5 -5
  36. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +4 -4
  38. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js +8 -8
  40. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/agent/settings/route.js +7 -7
  42. package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js +7 -7
  44. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/auth/[...all]/route.js +3 -3
  47. package/.next/standalone/.next/server/app/api/auth/[...all]/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/system/auth-config/route.js +5 -5
  49. package/.next/standalone/.next/server/app/api/system/auth-config/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/system/auth-settings/route.js +4 -4
  51. package/.next/standalone/.next/server/app/api/system/auth-settings/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/system/browse/route.js +6 -6
  53. package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/system/clear-root/route.js +5 -5
  55. package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/system/config/route.js +5 -5
  57. package/.next/standalone/.next/server/app/api/system/config/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/system/pins/route.js +4 -4
  59. package/.next/standalone/.next/server/app/api/system/pins/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/system/reveal/route.js +4 -4
  61. package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/api/system/root-status/route.js +4 -4
  63. package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/system/set-root/route.js +5 -5
  65. package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/wiki/app/route.js +6 -6
  67. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/api/wiki/content/route.js +7 -7
  69. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/api/wiki/folder/route.js +5 -5
  71. package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/api/wiki/move/route.js +5 -5
  73. package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js +5 -5
  75. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
  76. package/.next/standalone/.next/server/app/api/wiki/page/route.js +5 -5
  77. package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/api/wiki/route.js +5 -5
  79. package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
  80. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js +5 -5
  81. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/api/wiki/upload/route.js +5 -5
  83. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/api/wiki/watch/route.js +6 -6
  85. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  86. package/.next/standalone/.next/server/app/index.html +1 -1
  87. package/.next/standalone/.next/server/app/index.rsc +2 -2
  88. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  89. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  90. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  91. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  92. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  93. package/.next/standalone/.next/server/app/page.js +4 -4
  94. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  95. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  96. package/.next/standalone/.next/server/app/signin/page.js +9 -4
  97. package/.next/standalone/.next/server/app/signin/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__03h3~3e._.js → [root-of-the-server]__04udrya._.js} +2 -2
  100. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__04g0j8i._.js → [root-of-the-server]__0kdn1le._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0d89o98._.js → src_lib_auth_server_ts_00f4e0h._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/0biy_@better-auth_core_dist_121m_lh._.js +5 -0
  103. package/.next/standalone/.next/server/chunks/ssr/0dhv_better-auth_dist_adapters_kysely-adapter_index_mjs_08ymlcb._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/0dti_kysely_dist_esm_0-t3o9q._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/0dti_kysely_dist_esm_01._58q._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/ssr/0p91_next_0f40gy0._.js +6 -0
  107. package/.next/standalone/.next/server/chunks/ssr/{0p91_next_dist_0~2d1tl._.js → 0p91_next_dist_10ud_sa._.js} +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/0sag_@better-auth_memory-adapter_dist_index_mjs_07-_ka6._.js +3 -0
  109. package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_bun-sqlite-dialect-DzNwOpKv_mjs_0lruvb8._.js +3 -0
  110. package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_d1-sqlite-dialect-C2B7YsIT_mjs_03n6~tc._.js +3 -0
  111. package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_index_mjs_0gi5w.w._.js +4 -0
  112. package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_node-sqlite-dialect_mjs_0l4.y~0._.js +3 -0
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__026s8~z._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06mzg2t._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0btcd4o._.js +12 -0
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0c~og-9._.js +3 -0
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__134_q7l._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_051tcva._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0o~d81.._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/src_app_signin_00yk0vm._.js +452 -0
  122. package/.next/standalone/.next/server/chunks/ssr/src_app_signin_signin-form_tsx_0eznlbd._.js +3 -0
  123. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  124. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  125. package/.next/standalone/.next/server/pages/404.html +1 -1
  126. package/.next/standalone/.next/server/pages/500.html +1 -1
  127. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  128. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  129. package/.next/standalone/.next/static/chunks/09-jtayuhrq~b.js +1 -0
  130. package/.next/standalone/.next/static/chunks/0uv7ozzpiyp2_.js +1 -0
  131. package/.next/standalone/.next/static/chunks/135c~rq_ux73w.js +1 -0
  132. package/.next/standalone/docs/agent-fs-plan.md +65 -7
  133. package/.next/standalone/package.json +1 -1
  134. package/.next/standalone/src/app/signin/page.tsx +10 -225
  135. package/.next/standalone/src/app/signin/signin-form.tsx +238 -0
  136. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  137. package/package.json +1 -1
  138. package/.next/standalone/.next/server/app/signin.html +0 -1
  139. package/.next/standalone/.next/server/app/signin.meta +0 -15
  140. package/.next/standalone/.next/server/app/signin.rsc +0 -23
  141. package/.next/standalone/.next/server/app/signin.segments/_full.segment.rsc +0 -23
  142. package/.next/standalone/.next/server/app/signin.segments/_head.segment.rsc +0 -6
  143. package/.next/standalone/.next/server/app/signin.segments/_index.segment.rsc +0 -7
  144. package/.next/standalone/.next/server/app/signin.segments/_tree.segment.rsc +0 -3
  145. package/.next/standalone/.next/server/app/signin.segments/signin/__PAGE__.segment.rsc +0 -9
  146. package/.next/standalone/.next/server/app/signin.segments/signin.segment.rsc +0 -5
  147. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__088hqzy._.js +0 -3
  148. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0c147fo._.js +0 -3
  149. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0z.1v1z._.js +0 -3
  150. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12zp2dm._.js +0 -3
  151. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0exqvyf._.js +0 -3
  152. package/.next/standalone/.next/server/chunks/ssr/src_app_signin_page_tsx_1143y49._.js +0 -3
  153. package/.next/standalone/.next/static/chunks/0-_39e4awbk8y.js +0 -1
  154. package/.next/standalone/.next/static/chunks/02bxx~2cg~s6~.js +0 -1
  155. package/.next/standalone/.next/static/chunks/0mj_f8ncf2tks.js +0 -1
  156. /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → kzOuSAQArtvD7uiAJ6ZLh}/_buildManifest.js +0 -0
  157. /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → kzOuSAQArtvD7uiAJ6ZLh}/_clientMiddlewareManifest.js +0 -0
  158. /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → kzOuSAQArtvD7uiAJ6ZLh}/_ssgManifest.js +0 -0
@@ -0,0 +1 @@
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,419564,e=>{"use strict";var t=e.i(44501),s=e.i(101685),r=e.i(199075);e.s(["default",0,function({initialPasswordAuth:e,initialGoogle:n}){let[i,a]=(0,s.useState)("signin"),[o,l]=(0,s.useState)(""),[d,c]=(0,s.useState)(""),[u,m]=(0,s.useState)(""),[g,x]=(0,s.useState)(null),[p,f]=(0,s.useState)(!1),[h,b]=(0,s.useState)(n),[j,y]=(0,s.useState)(e),[w,v]=(0,s.useState)("/");async function N(e){e.preventDefault(),x(null),f(!0);try{if("signup"===i){let e=await r.authClient.signUp.email({email:o,password:d,name:u,callbackURL:w});e.error?x(e.error.message??"Sign-up failed"):window.location.href=w}else{let e=await r.authClient.signIn.email({email:o,password:d,callbackURL:w});if(e.error){let t=e.error.status;429===t?x("Too many sign-in attempts. Wait a minute and try again."):x(e.error.message??"Sign-in failed")}else window.location.href=w}}catch(e){x(e instanceof Error?e.message:"Unexpected error")}finally{f(!1)}}async function C(){x(null),f(!0);try{await r.authClient.signIn.social({provider:"google",callbackURL:w})}catch(e){x(e instanceof Error?e.message:"Google sign-in failed"),f(!1)}}return(0,s.useEffect)(()=>{fetch("/api/system/auth-config",{credentials:"include"}).then(e=>e.json()).then(e=>{e&&"object"==typeof e&&(b(!!e.google),y(!1!==e.passwordAuth))}).catch(()=>{});let e=new URLSearchParams(window.location.search).get("next");e&&e.startsWith("/")&&v(e)},[]),(0,t.jsx)("div",{className:"min-h-screen flex items-center justify-center bg-background",children:(0,t.jsxs)("div",{className:"w-full max-w-sm rounded-lg border border-border bg-card p-8 shadow-sm",children:[(0,t.jsx)("h1",{className:"mb-6 text-xl font-semibold text-foreground",children:"Sign in to wiki-viewer"}),h&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("button",{type:"button",onClick:C,disabled:p,className:"flex w-full items-center justify-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50",children:[(0,t.jsxs)("svg",{viewBox:"0 0 24 24",className:"h-4 w-4","aria-hidden":"true",children:[(0,t.jsx)("path",{fill:"#4285F4",d:"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"}),(0,t.jsx)("path",{fill:"#34A853",d:"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"}),(0,t.jsx)("path",{fill:"#FBBC05",d:"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"}),(0,t.jsx)("path",{fill:"#EA4335",d:"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"})]}),"Continue with Google"]}),j&&(0,t.jsxs)("div",{className:"my-4 flex items-center gap-3",children:[(0,t.jsx)("hr",{className:"flex-1 border-border"}),(0,t.jsx)("span",{className:"text-xs text-muted-foreground",children:"or"}),(0,t.jsx)("hr",{className:"flex-1 border-border"})]})]}),!j&&!h&&(0,t.jsx)("p",{className:"rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive",children:"No sign-in method is configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET, or enable email/password auth."}),j&&(0,t.jsxs)("form",{onSubmit:N,className:"space-y-4",children:["signup"===i&&(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"mb-1 block text-sm font-medium text-foreground",htmlFor:"name",children:"Name"}),(0,t.jsx)("input",{id:"name",type:"text",autoComplete:"name",value:u,onChange:e=>m(e.target.value),required:!0,className:"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"mb-1 block text-sm font-medium text-foreground",htmlFor:"email",children:"Email"}),(0,t.jsx)("input",{id:"email",type:"email",autoComplete:"email",value:o,onChange:e=>l(e.target.value),required:!0,className:"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"mb-1 block text-sm font-medium text-foreground",htmlFor:"password",children:"Password"}),(0,t.jsx)("input",{id:"password",type:"password",autoComplete:"signup"===i?"new-password":"current-password",value:d,onChange:e=>c(e.target.value),required:!0,minLength:8,className:"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"})]}),g&&(0,t.jsx)("p",{className:"rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive",children:g}),(0,t.jsx)("button",{type:"submit",disabled:p,className:"w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50",children:p?"signup"===i?"Creating account...":"Signing in...":"signup"===i?"Create account":"Sign in"})]}),j&&(0,t.jsxs)("p",{className:"mt-4 text-center text-sm text-muted-foreground",children:["signin"===i?"No account?":"Already have an account?"," ",(0,t.jsx)("button",{type:"button",onClick:()=>{a("signin"===i?"signup":"signin"),x(null)},className:"font-medium text-foreground underline-offset-4 hover:underline",children:"signin"===i?"Sign up":"Sign in"})]})]})})}])}]);
@@ -0,0 +1 @@
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,824627,t=>{"use strict";var e=function(t,r){return(e=Object.setPrototypeOf||({__proto__:[]})instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,r)},r=function(){return(r=Object.assign||function(t){for(var e,r=1,n=arguments.length;r<n;r++)for(var o in e=arguments[r])Object.prototype.hasOwnProperty.call(e,o)&&(t[o]=e[o]);return t}).apply(this,arguments)};"function"==typeof SuppressedError&&SuppressedError,t.s(["__assign",()=>r,"__awaiter",0,function(t,e,r,n){return new(r||(r=Promise))(function(o,c){function s(t){try{i(n.next(t))}catch(t){c(t)}}function a(t){try{i(n.throw(t))}catch(t){c(t)}}function i(t){var e;t.done?o(t.value):((e=t.value)instanceof r?e:new r(function(t){t(e)})).then(s,a)}i((n=n.apply(t,e||[])).next())})},"__extends",0,function(t,r){if("function"!=typeof r&&null!==r)throw TypeError("Class extends value "+String(r)+" is not a constructor or null");function n(){this.constructor=t}e(t,r),t.prototype=null===r?Object.create(r):(n.prototype=r.prototype,new n)},"__generator",0,function(t,e){var r,n,o,c={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},s=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return s.next=a(0),s.throw=a(1),s.return=a(2),"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(i){var l=[a,i];if(r)throw TypeError("Generator is already executing.");for(;s&&(s=0,l[0]&&(c=0)),c;)try{if(r=1,n&&(o=2&l[0]?n.return:l[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,l[1])).done)return o;switch(n=0,o&&(l=[2&l[0],o.value]),l[0]){case 0:case 1:o=l;break;case 4:return c.label++,{value:l[1],done:!1};case 5:c.label++,n=l[1],l=[0];continue;case 7:l=c.ops.pop(),c.trys.pop();continue;default:if(!(o=(o=c.trys).length>0&&o[o.length-1])&&(6===l[0]||2===l[0])){c=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]<o[3])){c.label=l[1];break}if(6===l[0]&&c.label<o[1]){c.label=o[1],o=l;break}if(o&&c.label<o[2]){c.label=o[2],c.ops.push(l);break}o[2]&&c.ops.pop(),c.trys.pop();continue}l=e.call(t,c)}catch(t){l=[6,t],n=0}finally{r=o=0}if(5&l[0])throw l[1];return{value:l[0]?l[1]:void 0,done:!0}}}},"__rest",0,function(t,e){var r={};for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&0>e.indexOf(n)&&(r[n]=t[n]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols)for(var o=0,n=Object.getOwnPropertySymbols(t);o<n.length;o++)0>e.indexOf(n[o])&&Object.prototype.propertyIsEnumerable.call(t,n[o])&&(r[n[o]]=t[n[o]]);return r},"__spreadArray",0,function(t,e,r){if(r||2==arguments.length)for(var n,o=0,c=e.length;o<c;o++)!n&&o in e||(n||(n=Array.prototype.slice.call(e,0,o)),n[o]=e[o]);return t.concat(n||Array.prototype.slice.call(e))}])},890793,t=>{t.v(e=>Promise.all(["static/chunks/14n5cp8rr-k_b.js","static/chunks/0~fvcxnmv6g_x.js","static/chunks/1674m~gp3w-8h.js","static/chunks/0~ikzdp3hga8n.js","static/chunks/0q0.5-57agpll.js","static/chunks/0q_xpywc4mr6r.js","static/chunks/0rldt2m_ic3fr.js","static/chunks/105.j2~dvdym3.js","static/chunks/0l4qtf8hgz.11.js"].map(e=>t.l(e))).then(()=>e(229414)))},410052,t=>{t.v(e=>Promise.all(["static/chunks/0pc3jws56z~cg.js","static/chunks/144e01xuvhx~x.js","static/chunks/0vp2gi~fq70je.js"].map(e=>t.l(e))).then(()=>e(116818)))},355350,t=>{t.v(e=>Promise.all(["static/chunks/0pc3jws56z~cg.js","static/chunks/09-jtayuhrq~b.js","static/chunks/144e01xuvhx~x.js"].map(e=>t.l(e))).then(()=>e(203010)))},432302,t=>{t.v(e=>Promise.all(["static/chunks/0pc3jws56z~cg.js","static/chunks/0qvlcjvhvpaey.js"].map(e=>t.l(e))).then(()=>e(924659)))}]);
@@ -62,9 +62,10 @@ All routes reuse `checkAuth` + `enforceScope` + `withFileMutex` + `safeRootPath`
62
62
  ### 2.2 Write (`PUT .../file/<path>`)
63
63
 
64
64
  - Atomic: write temp in **same dir** → `fsync` → `rename`. (`fsync` dir optional but documented.)
65
- - Preserve mode of existing file. Do **not** create parent dirs unless body/flag explicitly opts in.
66
- - `If-Match: <sha256>` 412 on mismatch. **Required by default for all writes** (see R4); explicit `?force=true` bypasses and is audited.
67
- - Response: `{path, sha256, size, mtime}`.
65
+ - Preserve mode of existing file. Do **not** create parent dirs unless `?mkdirs=true`.
66
+ - **Create = implicit.** `PUT` to a non-existent path creates the file (write-with-no-prior). Omit `If-Match` for a create; send `If-Match: <sha256>` only when overwriting. A create where the file already exists (no `If-Match`) 412, so creates can't silently clobber.
67
+ - `If-Match: <sha256>` → 412 on mismatch. **Required by default for overwrites** (see R4); explicit `?force=true` bypasses and is audited.
68
+ - Response: `{path, sha256, size, mtime, created: bool}`.
68
69
 
69
70
  ### 2.3 Listing (`GET .../ls`)
70
71
 
@@ -82,6 +83,12 @@ All routes reuse `checkAuth` + `enforceScope` + `withFileMutex` + `safeRootPath`
82
83
  - Checks: source `read`+`mutate`, dest `mutate`.
83
84
  - For `.md`: moves the `.proof/<path>.json` sidecar too. Lock ordering: single operation lock, or lock source+dest in **sorted key order** to avoid deadlock.
84
85
 
86
+ ### 2.6 Delete (`DELETE .../file/<path>`)
87
+
88
+ - Requires the `delete` scope op (see §3.5) and `If-Match: <sha256>` — the agent must have read the current file first, so it cannot blind-delete. This `If-Match` requirement **is** the confirmation for v1.
89
+ - `.md`: also deletes the sidecar (R3).
90
+ - Directory delete is opt-in and explicit: `?recursive=true`; without it, deleting a non-empty dir is refused. (No human approval queue in v1 — deferred.)
91
+
85
92
  ---
86
93
 
87
94
  ## 3. Tier 2 — Collab (unchanged)
@@ -90,14 +97,61 @@ Markdown block-ops (`block.replace/insertAfter/insertBefore/delete/append/prepen
90
97
 
91
98
  ---
92
99
 
100
+ ## 3.5 Working mode vs Collaborating mode (THE central distinction)
101
+
102
+ The whole design hinges on the agent knowing **when a file is being actively co-edited by a human** (use the reviewable Tier-2 path) versus **when it's just a file to work on** (use fast raw Tier-1). Today nothing tells the agent this; a wrong guess clobbers a human's pending suggestions. We make the mode **discoverable, documented, and enforced** — three layers so the agent cannot miss it.
103
+
104
+ ### The signal: `X-Collab-State` header (on every read, both tiers)
105
+
106
+ Every `GET fs/file/<path>` and every Tier-2 snapshot read returns:
107
+
108
+ ```
109
+ X-Collab-State: active | tracked | untracked | not-markdown
110
+ X-Collab-Snapshot: /api/agent/files/<path>.md # present when state != not-markdown
111
+ ```
112
+
113
+ | State | Meaning | Agent should… |
114
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
115
+ | `active` | `.md` that is **live**: either has review artifacts (pending suggestions / unresolved comments / unreverted proof-spans) **OR has a current human edit lease** (someone has the doc open in the editor). A human is collaborating right now. | **Use Tier-2 block-ops.** Your edits become reviewable suggestions. A raw `PUT` is rejected unless `If-Collab-Match` matches (see R6). |
116
+ | `tracked` | `.md` with a sidecar, no artifacts, no lease. | **Prefer Tier-2 for any semantic/prose edit** so provenance accrues. Raw only for mechanical/whole-file ops (e.g. reformat, regenerate). Not a free pass. |
117
+ | `untracked` | `.md`, no sidecar, no lease — nobody has collaborated. | Raw ok. A reviewable (Tier-2) edit creates a sidecar so future edits are tracked. |
118
+ | `not-markdown` | Any non-`.md` file. | **Tier-1 raw only** (Tier-2 doesn't apply). |
119
+
120
+ **Definition of `active` (two sources, OR'd):**
121
+
122
+ 1. **Artifacts:** `pendingSuggestions > 0` OR `unresolvedComments > 0` OR `proofSpanCount > 0` (from sidecar — cheap, already in memory on read).
123
+ 2. **Human edit lease:** a short-TTL presence marker (default 90s, heartbeat ~30s) set when a human opens the doc in the editor and refreshed while it stays open. **This closes the false-negative GPT-5.5 caught:** a human who opens a doc but hasn't typed a suggestion yet still reads `active`, so the agent won't blind-write into a live session. Lease lives in-memory (or the existing SQLite), keyed by `(path)`, set by a lightweight `POST /api/wiki/presence` ping the editor already can issue alongside its chokidar SSE connection. No lease + no artifacts → not active.
124
+
125
+ ### Layer 2 — documented (manifest + skill)
126
+
127
+ The `/api/agents/install` manifest and the skill state the rule in one sentence:
128
+
129
+ > _"Before editing a `.md`, read it and check `X-Collab-State`. If `active`, use block-ops (Tier 2) so a human can review. Otherwise use raw fs (Tier 1). For non-markdown, always use raw fs."_
130
+
131
+ ### Layer 3 — enforced (R4 + R6 backstop)
132
+
133
+ Even a confused or racing agent cannot silently clobber a live session: a raw `PUT` to an `active` `.md` is **rejected with 409 `COLLAB_ACTIVE`** unless it carries a matching `If-Collab-Match` (R6) — and the state is re-checked atomically inside the write mutex, so a session that goes active mid-flight still wins. Any genuinely orphaned suggestion/comment is marked `stale` by reconciliation (R2), never dropped. The 409 returns the Tier-2 snapshot URL, re-teaching the mode.
134
+
135
+ ### What the human sees
136
+
137
+ - Agent works in Tier-2 on an `active` doc → normal reviewable suggestions (existing UX).
138
+ - Agent raw-writes an `active` doc anyway → `file.rawWritten by ai:<id>` in the AI Panel feed + any unbindable anchors flagged `stale` (not vanished).
139
+ - Agent works on `untracked`/non-md → silent, fast, audited. No collab noise.
140
+
141
+ ---
142
+
93
143
  ## 4. Integration rules (load-bearing)
94
144
 
95
145
  - **R1 — Shared lock.** A raw write/move/delete on a `.md` acquires the **same** `withFileMutex` key the ops-applier uses. No interleaving with block-ops mid-proof-span.
96
146
  - **R2 — No fake provenance.** Raw writes never author proof-spans. After a raw write to a tracked `.md`, emit `file.rawWritten` (writer known) and **reconcile the sidecar synchronously, inside the same mutex** (rebuild `refMap`, set fingerprint to `newSha`, mark affected proof refs stale). Raw edits are unmarked by definition; human sees _who_ via the event, not as an accept/revert suggestion.
97
147
  - **GOTCHA (do not regress):** do NOT just "bump fingerprint to newSha and let existing reconciliation fire later." The existing reconciliation triggers on `sidecar.fingerprint != file sha256`; if the raw write sets fingerprint current, the mismatch is gone and rebuild never runs. Because the writer is known, reconcile **eagerly within the write**, not lazily on next read.
98
- - **R3 — Sidecar lifecycle.** Raw `mv`/`rm` of a `.md` moves/deletes its sidecar (reuse human `wiki/move` logic).
148
+ - **R3 — Sidecar lifecycle.** Raw `mv`/`rm` of a `.md` moves/deletes its sidecar. **NOTE:** the human `wiki/move` route currently does a bare `rename` and does NOT move the sidecar — a latent bug that orphans `.proof/` JSON today. Build a shared `moveSidecar`/`deleteSidecar` helper in `sidecar.ts` and wire **both** the raw-fs path and `wiki/move` to it (fixes the existing bug for free).
99
149
  - **R4 — Write guard (If-Match by default).** **All** mutating raw ops (`PUT`/`DELETE`/`move`) REQUIRE `If-Match: <sha256>` by default; 412 on mismatch. An explicit `?force=true` bypasses it and is recorded in the audit row. Rationale: agents edit code/config just as much as `.md`; optional concurrency = silent lost updates, and a format-based safety boundary is arbitrary. Cheap because the agent already has the sha from `GET`.
100
- - **R5 — Scope unification.** Registration `scope.paths` glob + `scope.ops:[read,mutate]` already exist; raw-fs honors the same. No new scope concepts in v1.
150
+ - **R5 — Scope unification + `delete` op.** Registration `scope.paths` glob already covers **directories natively** (e.g. `notes/**` grants the whole subtree — no file lists). Raw-fs honors the same glob, on every surface (read/ls/search/move/delete results all re-checked). **One addition:** split `delete` out of `mutate`, so `scope.ops` becomes `[read, mutate, delete]`. This lets a human grant "edit but never delete." `mutate` = create/overwrite/move; `delete` = remove. Back-compat: existing `[read, mutate]` agents simply can't delete until re-scoped. Glob dialect (v1): `**`, `*`, `?` only — no braces/negation; advertise this in the manifest.
151
+ - **R6 — Collab-state precondition on raw `.md` writes (closes the TOCTOU race).** `If-Match` protects _bytes_, not _collaboration intent_: a human could create a sidecar/lease after the agent's read but before its raw `PUT`, leaving bytes unchanged so `If-Match` still passes. To prevent silently writing into a session that went `active` mid-flight:
152
+ - Every `.md` read also returns `X-Collab-Revision: <n>` (bumped on any sidecar/lease state change).
153
+ - A raw `PUT` to a `.md` must, **inside the same mutex, re-read collab state immediately before writing**: if the doc is now `active` and the request did not supply a matching `If-Collab-Match: <n>`, **reject with 409 `COLLAB_ACTIVE`** + the Tier-2 snapshot URL. The agent then switches to block-ops. `?force=true` still bypasses (audited) for deliberate overrides.
154
+ - This makes the mode check atomic with the write, not advisory.
101
155
 
102
156
  ### Event taxonomy (don't overload)
103
157
 
@@ -120,7 +174,8 @@ A full-file `PUT` to a `.md` can strip `<proof-span>` marks and shift block boun
120
174
  Extend `/api/agents/install` manifest:
121
175
 
122
176
  - Advertise the new `fs/*` routes.
123
- - Add a `capabilities` block: `{ maxFileBytes, supportsRange, ifMatchRequired:true, forceBypass:true, search:["grep","glob"] }`.
177
+ - Add a `capabilities` block: `{ maxFileBytes, supportsRange, ifMatchRequired:true, forceBypass:true, search:["grep","glob"], globDialect:"**,*,?", scopeOps:["read","mutate","delete"], collabStates:["active","tracked","untracked","not-markdown"], collabPrecondition:"If-Collab-Match" }`.
178
+ - State the **working-vs-collaborating rule** (§3.5) in prose so the agent self-configures its tier choice.
124
179
  - Advertise the optional MCP adapter package + version.
125
180
  One manifest; agent self-configures. HTTP routes are the canonical contract.
126
181
 
@@ -135,6 +190,8 @@ Extend `/api/agents/install` manifest:
135
190
  - `edit_file` (str-replace) → **client-side**: read → transform → `PUT If-Match`. (Server stays dumb; edit footguns live in the adapter.)
136
191
  - `list_directory` → `GET fs/ls`
137
192
  - `search` → `POST fs/search`
193
+ - `delete_file` → `DELETE fs/file` (requires `delete` scope + `If-Match`)
194
+ - The shim reads `X-Collab-State` and, when `active`, **warns or routes the edit through the Tier-2 block-op tools** instead of a blind raw write — so even off-the-shelf MCP clients respect collaborating mode.
138
195
  - Do **not** make the core app MCP-native first. The app already has HTTP auth/discovery; the shim adapts to Claude Code / Cursor / Codex without poisoning the core.
139
196
 
140
197
  ---
@@ -165,7 +222,8 @@ Extend `/api/agents/install` manifest:
165
222
  5. `fs/search` (grep/glob, limits).
166
223
  6. Extend `/api/agents/install` manifest + capabilities.
167
224
  7. `wiki-viewer-mcp` adapter (read/write/edit/list/search).
168
- 8. Tests: traversal, symlink escape, If-Match 412, sidecar move, scope filtering on ls/search, big-file Range, binary skip in grep.
225
+ 8. **Mode plumbing:** human edit-lease (`POST /api/wiki/presence` + TTL, editor heartbeat), `X-Collab-State` + `X-Collab-Revision` headers on both read paths, R6 atomic re-check + 409 `COLLAB_ACTIVE` on raw `.md` writes. Plus `delete` scope op + create/`?mkdirs`/`?recursive` semantics.
226
+ 9. Tests: traversal, symlink escape, If-Match 412, create-collision 412, sidecar move (incl. `wiki/move` bug fix), scope filtering on ls/search, big-file Range, binary skip in grep, **`X-Collab-State` matrix incl. lease-only `active`**, **R6 TOCTOU: doc goes active between read and raw PUT → 409**, stale-anchor after raw `.md` overwrite, `delete` op gating, lease expiry flips active→tracked.
169
227
 
170
228
  ```
171
229
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-viewer",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Local file viewer and wiki — view markdown, HTML, PDFs, notebooks, office docs, and more from any directory",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,231 +1,16 @@
1
- "use client";
1
+ import SignInForm from "./signin-form";
2
+ import { passwordAuthEnabled } from "@/lib/auth/server";
2
3
 
3
- import { useState, useEffect, type FormEvent } from "react";
4
- import { authClient } from "@/lib/auth/client";
4
+ // Server component: resolve the available auth methods at render time so the
5
+ // initial HTML already reflects the configuration. This prevents the password
6
+ // form from flashing on load when it is disabled (AUTH_DISABLE_PASSWORD=1).
7
+ export const dynamic = "force-dynamic";
5
8
 
6
9
  export default function SignInPage() {
7
- const [mode, setMode] = useState<"signin" | "signup">("signin");
8
- const [email, setEmail] = useState("");
9
- const [password, setPassword] = useState("");
10
- const [name, setName] = useState("");
11
- const [error, setError] = useState<string | null>(null);
12
- const [loading, setLoading] = useState(false);
13
- const [hasGoogle, setHasGoogle] = useState(false);
14
- const [passwordAuth, setPasswordAuth] = useState(true);
15
- const [callbackURL, setCallbackURL] = useState("/");
16
-
17
- useEffect(() => {
18
- // Learn which auth methods to show. Public endpoint, no session needed.
19
- fetch("/api/system/auth-config", { credentials: "include" })
20
- .then((r) => r.json())
21
- .then((data: unknown) => {
22
- if (data && typeof data === "object") {
23
- const cfg = data as { google?: boolean; passwordAuth?: boolean };
24
- setHasGoogle(Boolean(cfg.google));
25
- // Default to showing the password form unless told otherwise, so a
26
- // failed fetch never leaves the user with no way to sign in.
27
- setPasswordAuth(cfg.passwordAuth !== false);
28
- }
29
- })
30
- .catch(() => {});
31
-
32
- // Honour ?next= redirect param
33
- const params = new URLSearchParams(window.location.search);
34
- const next = params.get("next");
35
- if (next && next.startsWith("/")) setCallbackURL(next);
36
- }, []);
37
-
38
- async function handleSubmit(e: FormEvent) {
39
- e.preventDefault();
40
- setError(null);
41
- setLoading(true);
42
- try {
43
- if (mode === "signup") {
44
- const res = await authClient.signUp.email({
45
- email,
46
- password,
47
- name,
48
- callbackURL,
49
- });
50
- if (res.error) {
51
- setError(res.error.message ?? "Sign-up failed");
52
- } else {
53
- window.location.href = callbackURL;
54
- }
55
- } else {
56
- const res = await authClient.signIn.email({
57
- email,
58
- password,
59
- callbackURL,
60
- });
61
- if (res.error) {
62
- const status = res.error.status;
63
- if (status === 429) {
64
- setError("Too many sign-in attempts. Wait a minute and try again.");
65
- } else {
66
- setError(res.error.message ?? "Sign-in failed");
67
- }
68
- } else {
69
- window.location.href = callbackURL;
70
- }
71
- }
72
- } catch (err: unknown) {
73
- setError(err instanceof Error ? err.message : "Unexpected error");
74
- } finally {
75
- setLoading(false);
76
- }
77
- }
78
-
79
- async function handleGoogle() {
80
- setError(null);
81
- setLoading(true);
82
- try {
83
- await authClient.signIn.social({ provider: "google", callbackURL });
84
- } catch (err: unknown) {
85
- setError(err instanceof Error ? err.message : "Google sign-in failed");
86
- setLoading(false);
87
- }
88
- }
89
-
10
+ const google = Boolean(
11
+ process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET,
12
+ );
90
13
  return (
91
- <div className="min-h-screen flex items-center justify-center bg-background">
92
- <div className="w-full max-w-sm rounded-lg border border-border bg-card p-8 shadow-sm">
93
- <h1 className="mb-6 text-xl font-semibold text-foreground">Sign in to wiki-viewer</h1>
94
-
95
- {hasGoogle && (
96
- <>
97
- <button
98
- type="button"
99
- onClick={handleGoogle}
100
- disabled={loading}
101
- className="flex w-full items-center justify-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50"
102
- >
103
- <svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
104
- <path
105
- fill="#4285F4"
106
- d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
107
- />
108
- <path
109
- fill="#34A853"
110
- d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
111
- />
112
- <path
113
- fill="#FBBC05"
114
- d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
115
- />
116
- <path
117
- fill="#EA4335"
118
- d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
119
- />
120
- </svg>
121
- Continue with Google
122
- </button>
123
- {passwordAuth && (
124
- <div className="my-4 flex items-center gap-3">
125
- <hr className="flex-1 border-border" />
126
- <span className="text-xs text-muted-foreground">or</span>
127
- <hr className="flex-1 border-border" />
128
- </div>
129
- )}
130
- </>
131
- )}
132
-
133
- {!passwordAuth && !hasGoogle && (
134
- <p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
135
- No sign-in method is configured. Set GOOGLE_CLIENT_ID and
136
- GOOGLE_CLIENT_SECRET, or enable email/password auth.
137
- </p>
138
- )}
139
-
140
- {passwordAuth && (
141
- <form onSubmit={handleSubmit} className="space-y-4">
142
- {mode === "signup" && (
143
- <div>
144
- <label className="mb-1 block text-sm font-medium text-foreground" htmlFor="name">
145
- Name
146
- </label>
147
- <input
148
- id="name"
149
- type="text"
150
- autoComplete="name"
151
- value={name}
152
- onChange={(e) => setName(e.target.value)}
153
- required
154
- className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
155
- />
156
- </div>
157
- )}
158
- <div>
159
- <label className="mb-1 block text-sm font-medium text-foreground" htmlFor="email">
160
- Email
161
- </label>
162
- <input
163
- id="email"
164
- type="email"
165
- autoComplete="email"
166
- value={email}
167
- onChange={(e) => setEmail(e.target.value)}
168
- required
169
- className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
170
- />
171
- </div>
172
- <div>
173
- <label
174
- className="mb-1 block text-sm font-medium text-foreground"
175
- htmlFor="password"
176
- >
177
- Password
178
- </label>
179
- <input
180
- id="password"
181
- type="password"
182
- autoComplete={mode === "signup" ? "new-password" : "current-password"}
183
- value={password}
184
- onChange={(e) => setPassword(e.target.value)}
185
- required
186
- minLength={8}
187
- className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
188
- />
189
- </div>
190
-
191
- {error && (
192
- <p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
193
- {error}
194
- </p>
195
- )}
196
-
197
- <button
198
- type="submit"
199
- disabled={loading}
200
- className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
201
- >
202
- {loading
203
- ? mode === "signup"
204
- ? "Creating account..."
205
- : "Signing in..."
206
- : mode === "signup"
207
- ? "Create account"
208
- : "Sign in"}
209
- </button>
210
- </form>
211
- )}
212
-
213
- {passwordAuth && (
214
- <p className="mt-4 text-center text-sm text-muted-foreground">
215
- {mode === "signin" ? "No account?" : "Already have an account?"}{" "}
216
- <button
217
- type="button"
218
- onClick={() => {
219
- setMode(mode === "signin" ? "signup" : "signin");
220
- setError(null);
221
- }}
222
- className="font-medium text-foreground underline-offset-4 hover:underline"
223
- >
224
- {mode === "signin" ? "Sign up" : "Sign in"}
225
- </button>
226
- </p>
227
- )}
228
- </div>
229
- </div>
14
+ <SignInForm initialPasswordAuth={passwordAuthEnabled} initialGoogle={google} />
230
15
  );
231
16
  }
@@ -0,0 +1,238 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, type FormEvent } from "react";
4
+ import { authClient } from "@/lib/auth/client";
5
+
6
+ export default function SignInForm({
7
+ initialPasswordAuth,
8
+ initialGoogle,
9
+ }: {
10
+ initialPasswordAuth: boolean;
11
+ initialGoogle: boolean;
12
+ }) {
13
+ const [mode, setMode] = useState<"signin" | "signup">("signin");
14
+ const [email, setEmail] = useState("");
15
+ const [password, setPassword] = useState("");
16
+ const [name, setName] = useState("");
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [loading, setLoading] = useState(false);
19
+ // Seed from server-rendered values so the first paint already matches the
20
+ // configured auth methods. This avoids a flash of the password form when it
21
+ // is disabled. The fetch below only refreshes in case config changed.
22
+ const [hasGoogle, setHasGoogle] = useState(initialGoogle);
23
+ const [passwordAuth, setPasswordAuth] = useState(initialPasswordAuth);
24
+ const [callbackURL, setCallbackURL] = useState("/");
25
+
26
+ useEffect(() => {
27
+ // Refresh which auth methods to show. Public endpoint, no session needed.
28
+ fetch("/api/system/auth-config", { credentials: "include" })
29
+ .then((r) => r.json())
30
+ .then((data: unknown) => {
31
+ if (data && typeof data === "object") {
32
+ const cfg = data as { google?: boolean; passwordAuth?: boolean };
33
+ setHasGoogle(Boolean(cfg.google));
34
+ setPasswordAuth(cfg.passwordAuth !== false);
35
+ }
36
+ })
37
+ .catch(() => {});
38
+
39
+ // Honour ?next= redirect param
40
+ const params = new URLSearchParams(window.location.search);
41
+ const next = params.get("next");
42
+ if (next && next.startsWith("/")) setCallbackURL(next);
43
+ }, []);
44
+
45
+ async function handleSubmit(e: FormEvent) {
46
+ e.preventDefault();
47
+ setError(null);
48
+ setLoading(true);
49
+ try {
50
+ if (mode === "signup") {
51
+ const res = await authClient.signUp.email({
52
+ email,
53
+ password,
54
+ name,
55
+ callbackURL,
56
+ });
57
+ if (res.error) {
58
+ setError(res.error.message ?? "Sign-up failed");
59
+ } else {
60
+ window.location.href = callbackURL;
61
+ }
62
+ } else {
63
+ const res = await authClient.signIn.email({
64
+ email,
65
+ password,
66
+ callbackURL,
67
+ });
68
+ if (res.error) {
69
+ const status = res.error.status;
70
+ if (status === 429) {
71
+ setError("Too many sign-in attempts. Wait a minute and try again.");
72
+ } else {
73
+ setError(res.error.message ?? "Sign-in failed");
74
+ }
75
+ } else {
76
+ window.location.href = callbackURL;
77
+ }
78
+ }
79
+ } catch (err: unknown) {
80
+ setError(err instanceof Error ? err.message : "Unexpected error");
81
+ } finally {
82
+ setLoading(false);
83
+ }
84
+ }
85
+
86
+ async function handleGoogle() {
87
+ setError(null);
88
+ setLoading(true);
89
+ try {
90
+ await authClient.signIn.social({ provider: "google", callbackURL });
91
+ } catch (err: unknown) {
92
+ setError(err instanceof Error ? err.message : "Google sign-in failed");
93
+ setLoading(false);
94
+ }
95
+ }
96
+
97
+ return (
98
+ <div className="min-h-screen flex items-center justify-center bg-background">
99
+ <div className="w-full max-w-sm rounded-lg border border-border bg-card p-8 shadow-sm">
100
+ <h1 className="mb-6 text-xl font-semibold text-foreground">Sign in to wiki-viewer</h1>
101
+
102
+ {hasGoogle && (
103
+ <>
104
+ <button
105
+ type="button"
106
+ onClick={handleGoogle}
107
+ disabled={loading}
108
+ className="flex w-full items-center justify-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50"
109
+ >
110
+ <svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
111
+ <path
112
+ fill="#4285F4"
113
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
114
+ />
115
+ <path
116
+ fill="#34A853"
117
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
118
+ />
119
+ <path
120
+ fill="#FBBC05"
121
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
122
+ />
123
+ <path
124
+ fill="#EA4335"
125
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
126
+ />
127
+ </svg>
128
+ Continue with Google
129
+ </button>
130
+ {passwordAuth && (
131
+ <div className="my-4 flex items-center gap-3">
132
+ <hr className="flex-1 border-border" />
133
+ <span className="text-xs text-muted-foreground">or</span>
134
+ <hr className="flex-1 border-border" />
135
+ </div>
136
+ )}
137
+ </>
138
+ )}
139
+
140
+ {!passwordAuth && !hasGoogle && (
141
+ <p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
142
+ No sign-in method is configured. Set GOOGLE_CLIENT_ID and
143
+ GOOGLE_CLIENT_SECRET, or enable email/password auth.
144
+ </p>
145
+ )}
146
+
147
+ {passwordAuth && (
148
+ <form onSubmit={handleSubmit} className="space-y-4">
149
+ {mode === "signup" && (
150
+ <div>
151
+ <label className="mb-1 block text-sm font-medium text-foreground" htmlFor="name">
152
+ Name
153
+ </label>
154
+ <input
155
+ id="name"
156
+ type="text"
157
+ autoComplete="name"
158
+ value={name}
159
+ onChange={(e) => setName(e.target.value)}
160
+ required
161
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
162
+ />
163
+ </div>
164
+ )}
165
+ <div>
166
+ <label className="mb-1 block text-sm font-medium text-foreground" htmlFor="email">
167
+ Email
168
+ </label>
169
+ <input
170
+ id="email"
171
+ type="email"
172
+ autoComplete="email"
173
+ value={email}
174
+ onChange={(e) => setEmail(e.target.value)}
175
+ required
176
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
177
+ />
178
+ </div>
179
+ <div>
180
+ <label
181
+ className="mb-1 block text-sm font-medium text-foreground"
182
+ htmlFor="password"
183
+ >
184
+ Password
185
+ </label>
186
+ <input
187
+ id="password"
188
+ type="password"
189
+ autoComplete={mode === "signup" ? "new-password" : "current-password"}
190
+ value={password}
191
+ onChange={(e) => setPassword(e.target.value)}
192
+ required
193
+ minLength={8}
194
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
195
+ />
196
+ </div>
197
+
198
+ {error && (
199
+ <p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
200
+ {error}
201
+ </p>
202
+ )}
203
+
204
+ <button
205
+ type="submit"
206
+ disabled={loading}
207
+ className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
208
+ >
209
+ {loading
210
+ ? mode === "signup"
211
+ ? "Creating account..."
212
+ : "Signing in..."
213
+ : mode === "signup"
214
+ ? "Create account"
215
+ : "Sign in"}
216
+ </button>
217
+ </form>
218
+ )}
219
+
220
+ {passwordAuth && (
221
+ <p className="mt-4 text-center text-sm text-muted-foreground">
222
+ {mode === "signin" ? "No account?" : "Already have an account?"}{" "}
223
+ <button
224
+ type="button"
225
+ onClick={() => {
226
+ setMode(mode === "signin" ? "signup" : "signin");
227
+ setError(null);
228
+ }}
229
+ className="font-medium text-foreground underline-offset-4 hover:underline"
230
+ >
231
+ {mode === "signin" ? "Sign up" : "Sign in"}
232
+ </button>
233
+ </p>
234
+ )}
235
+ </div>
236
+ </div>
237
+ );
238
+ }