smoodly 0.0.51 → 0.0.53

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 (292) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth.d.ts +3 -11
  3. package/dist/admin/auth.js +1 -30
  4. package/dist/admin/client-ops.d.ts +5 -4
  5. package/dist/admin/client-ops.js +15 -12
  6. package/dist/admin/delivery-mode.d.ts +1 -1
  7. package/dist/admin/delivery-mode.js +2 -2
  8. package/dist/admin/drift.d.ts +0 -2
  9. package/dist/admin/drift.js +1 -1
  10. package/dist/admin/editor/EditorView.d.ts +0 -3
  11. package/dist/admin/editor/EditorView.js +11 -16
  12. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  13. package/dist/admin/editor/FloatingToolbar.js +1 -1
  14. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  15. package/dist/admin/editor/SeoSettings.js +2 -2
  16. package/dist/admin/editor/canvas-address.d.ts +8 -0
  17. package/dist/admin/editor/canvas-address.js +18 -2
  18. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  19. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  20. package/dist/admin/editor/inline-dom.d.ts +0 -4
  21. package/dist/admin/editor/inline-dom.js +1 -1
  22. package/dist/admin/editor/preview-grant.d.ts +6 -0
  23. package/dist/admin/editor/preview-grant.js +25 -0
  24. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  25. package/dist/admin/files.generated.js +1 -1
  26. package/dist/admin/fixed-nodes.d.ts +0 -6
  27. package/dist/admin/fixed-nodes.js +1 -81
  28. package/dist/admin/forms/CropEditor.d.ts +4 -2
  29. package/dist/admin/forms/CropEditor.js +58 -5
  30. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  31. package/dist/admin/forms/ImagePreview.js +9 -3
  32. package/dist/admin/forms/MediaGrid.js +2 -1
  33. package/dist/admin/forms/MediaPicker.js +9 -6
  34. package/dist/admin/forms/MediaWidget.js +11 -9
  35. package/dist/admin/forms/assets-context.d.ts +7 -3
  36. package/dist/admin/forms/assets-context.js +5 -3
  37. package/dist/admin/forms/crop-editor.d.ts +24 -2
  38. package/dist/admin/forms/crop-editor.js +36 -2
  39. package/dist/admin/forms/date-picker.d.ts +0 -1
  40. package/dist/admin/forms/date-picker.js +1 -1
  41. package/dist/admin/forms/tabs.d.ts +0 -1
  42. package/dist/admin/forms/tabs.js +1 -1
  43. package/dist/admin/forms/upload.d.ts +18 -11
  44. package/dist/admin/forms/upload.js +62 -24
  45. package/dist/admin/global-items.d.ts +0 -3
  46. package/dist/admin/global-items.js +0 -54
  47. package/dist/admin/handler.d.ts +22 -28
  48. package/dist/admin/handler.js +146 -47
  49. package/dist/admin/health.d.ts +0 -4
  50. package/dist/admin/index.d.ts +6 -9
  51. package/dist/admin/index.js +3 -6
  52. package/dist/admin/ops.d.ts +40 -32
  53. package/dist/admin/page-data.d.ts +14 -6
  54. package/dist/admin/page-data.js +1 -1
  55. package/dist/admin/session-flow.d.ts +65 -0
  56. package/dist/admin/session-flow.js +121 -0
  57. package/dist/admin/shell/AdminApp.d.ts +8 -3
  58. package/dist/admin/shell/AdminApp.js +37 -58
  59. package/dist/admin/shell/ContentHealthPage.js +1 -3
  60. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  61. package/dist/admin/shell/GlobalFields.js +1 -1
  62. package/dist/admin/shell/IconRail.js +10 -8
  63. package/dist/admin/shell/MediaScreen.js +9 -6
  64. package/dist/admin/shell/MembersPage.d.ts +1 -2
  65. package/dist/admin/shell/MembersPage.js +19 -90
  66. package/dist/admin/shell/SettingsView.d.ts +1 -2
  67. package/dist/admin/shell/SettingsView.js +2 -2
  68. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  69. package/dist/admin/shell/SignedOutView.js +9 -0
  70. package/dist/admin/shell/console-links.d.ts +5 -0
  71. package/dist/admin/shell/console-links.js +11 -0
  72. package/dist/admin/shell/members-list.d.ts +1 -14
  73. package/dist/admin/shell/members-list.js +2 -23
  74. package/dist/admin/shell/pages-tree.d.ts +0 -2
  75. package/dist/admin/shell/pages-tree.js +1 -1
  76. package/dist/admin/shell/presence-socket.d.ts +10 -0
  77. package/dist/admin/shell/presence-socket.js +191 -0
  78. package/dist/admin/shell/presence.d.ts +6 -18
  79. package/dist/admin/shell/presence.js +4 -11
  80. package/dist/admin/shell/session.d.ts +19 -12
  81. package/dist/admin/shell/session.js +114 -47
  82. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  83. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  84. package/dist/admin/ui/primitives.d.ts +4 -14
  85. package/dist/admin/ui/primitives.js +7 -9
  86. package/dist/admin/ui/theme.js +13 -2
  87. package/dist/admin/validate.js +10 -1
  88. package/dist/api-client.d.ts +26 -0
  89. package/dist/api-client.js +107 -0
  90. package/dist/assemble.d.ts +4 -41
  91. package/dist/assemble.js +5 -303
  92. package/dist/asset-store.d.ts +28 -30
  93. package/dist/asset-store.js +26 -121
  94. package/dist/astro/cache.d.ts +1 -1
  95. package/dist/astro/components/Head.astro +4 -1
  96. package/dist/astro/components/Image.astro +12 -17
  97. package/dist/astro/components/Video.astro +5 -2
  98. package/dist/astro/head.d.ts +3 -0
  99. package/dist/astro/head.js +24 -0
  100. package/dist/astro/index.js +61 -27
  101. package/dist/astro/locale-paths.d.ts +5 -2
  102. package/dist/astro/locale-paths.js +10 -2
  103. package/dist/astro/routes/admin.js +9 -7
  104. package/dist/astro/runtime.d.ts +3 -2
  105. package/dist/astro/runtime.js +5 -4
  106. package/dist/astro/server.d.ts +7 -11
  107. package/dist/astro/server.js +22 -28
  108. package/dist/astro/shell.d.ts +3 -3
  109. package/dist/astro/shell.js +1 -1
  110. package/dist/astro/static-media-node.d.ts +4 -0
  111. package/dist/astro/static-media-node.js +25 -0
  112. package/dist/astro/static-media.d.ts +17 -0
  113. package/dist/astro/static-media.js +55 -0
  114. package/dist/astro/static.d.ts +5 -10
  115. package/dist/astro/static.js +13 -29
  116. package/dist/canvas-marker.d.ts +14 -0
  117. package/dist/canvas-marker.js +36 -0
  118. package/dist/cli/api.d.ts +14 -0
  119. package/dist/cli/api.js +48 -0
  120. package/dist/cli/app-env.d.ts +2 -0
  121. package/dist/cli/app-env.js +20 -0
  122. package/dist/cli/export.d.ts +16 -2
  123. package/dist/cli/export.js +52 -35
  124. package/dist/cli/import.d.ts +7 -11
  125. package/dist/cli/import.js +50 -54
  126. package/dist/cli/load-config.d.ts +0 -1
  127. package/dist/cli/load-config.js +1 -1
  128. package/dist/cli/main.d.ts +4 -12
  129. package/dist/cli/main.js +26 -72
  130. package/dist/cli/push.d.ts +6 -0
  131. package/dist/cli/push.js +31 -0
  132. package/dist/collections.d.ts +0 -1
  133. package/dist/collections.js +1 -1
  134. package/dist/config.d.ts +20 -5
  135. package/dist/config.js +18 -1
  136. package/dist/content/content-api.d.ts +23 -23
  137. package/dist/content/content-api.js +48 -56
  138. package/dist/content/mime.d.ts +1 -0
  139. package/dist/content/mime.js +6 -0
  140. package/dist/css.js +1 -1
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/entry-store.d.ts +8 -94
  143. package/dist/entry-store.js +9 -557
  144. package/dist/env.d.ts +27 -20
  145. package/dist/env.js +38 -50
  146. package/dist/fields.d.ts +11 -3
  147. package/dist/fields.js +33 -2
  148. package/dist/global-id.d.ts +2 -0
  149. package/dist/global-id.js +4 -2
  150. package/dist/image/SmoodlyImage.d.ts +9 -5
  151. package/dist/image/SmoodlyImage.js +29 -15
  152. package/dist/image/SmoodlyVideo.d.ts +1 -1
  153. package/dist/image/SmoodlyVideo.js +3 -2
  154. package/dist/image/index.d.ts +2 -3
  155. package/dist/image/index.js +0 -2
  156. package/dist/image/markup.d.ts +53 -0
  157. package/dist/image/markup.js +111 -0
  158. package/dist/index.d.ts +13 -23
  159. package/dist/index.js +7 -20
  160. package/dist/media-cut.d.ts +44 -0
  161. package/dist/media-cut.js +70 -0
  162. package/dist/media.d.ts +58 -4
  163. package/dist/media.js +126 -16
  164. package/dist/member-store.d.ts +2 -50
  165. package/dist/member-store.js +0 -56
  166. package/dist/next/admin-route.d.ts +1 -1
  167. package/dist/next/admin-route.js +5 -3
  168. package/dist/next/meta.js +2 -3
  169. package/dist/next/page-renderer.d.ts +2 -2
  170. package/dist/next/page-renderer.js +22 -11
  171. package/dist/next/queries.d.ts +2 -2
  172. package/dist/path-index.d.ts +0 -24
  173. package/dist/path-index.js +0 -98
  174. package/dist/paths.d.ts +1 -1
  175. package/dist/paths.js +1 -1
  176. package/dist/push.d.ts +30 -0
  177. package/dist/push.js +52 -0
  178. package/dist/read-counter.d.ts +2 -0
  179. package/dist/read-counter.js +6 -0
  180. package/dist/reads.d.ts +7 -7
  181. package/dist/reads.js +3 -0
  182. package/dist/registry-json.d.ts +11 -0
  183. package/dist/registry-json.js +81 -0
  184. package/dist/render.d.ts +0 -1
  185. package/dist/render.js +1 -1
  186. package/dist/resolve.d.ts +12 -6
  187. package/dist/resolve.js +11 -8
  188. package/dist/revalidate.d.ts +8 -1
  189. package/dist/revalidate.js +8 -1
  190. package/dist/richtext/index.d.ts +1 -1
  191. package/dist/section-wrapper.d.ts +1 -1
  192. package/dist/section-wrapper.js +0 -9
  193. package/dist/signed-message.d.ts +26 -0
  194. package/dist/signed-message.js +103 -0
  195. package/dist/site.d.ts +59 -18
  196. package/dist/site.js +21 -252
  197. package/dist/space-transfer.d.ts +6 -3
  198. package/dist/space-transfer.js +18 -6
  199. package/dist/store.d.ts +2 -80
  200. package/dist/store.js +3 -489
  201. package/dist/toolset.d.ts +2 -1
  202. package/dist/toolset.js +1 -1
  203. package/dist/version.d.ts +1 -1
  204. package/dist/version.js +1 -1
  205. package/package.json +4 -15
  206. package/dist/admin/auth-config.d.ts +0 -6
  207. package/dist/admin/browser/fetch-op.d.ts +0 -10
  208. package/dist/admin/browser/fetch-op.js +0 -44
  209. package/dist/admin/browser/main.d.ts +0 -1
  210. package/dist/admin/browser/main.js +0 -23
  211. package/dist/admin/ops-impl.d.ts +0 -51
  212. package/dist/admin/ops-impl.js +0 -1403
  213. package/dist/admin/shell/InviteModal.d.ts +0 -6
  214. package/dist/admin/shell/InviteModal.js +0 -28
  215. package/dist/admin/shell/LoginView.d.ts +0 -5
  216. package/dist/admin/shell/LoginView.js +0 -80
  217. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  218. package/dist/admin/shell/SchemaNotice.js +0 -4
  219. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  220. package/dist/admin/shell/SetPasswordView.js +0 -41
  221. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  222. package/dist/admin/shell/presence-realtime.js +0 -32
  223. package/dist/admin/supabase-auth.d.ts +0 -7
  224. package/dist/admin/supabase-auth.js +0 -30
  225. package/dist/astro/deploy-deps.d.ts +0 -10
  226. package/dist/astro/deploy-deps.js +0 -8
  227. package/dist/astro/image.d.ts +0 -27
  228. package/dist/astro/image.js +0 -29
  229. package/dist/bind.d.ts +0 -19
  230. package/dist/bind.js +0 -34
  231. package/dist/cli/add-admin.d.ts +0 -11
  232. package/dist/cli/add-admin.js +0 -25
  233. package/dist/cli/load-collections.d.ts +0 -2
  234. package/dist/cli/load-collections.js +0 -27
  235. package/dist/cli/migrate.d.ts +0 -43
  236. package/dist/cli/migrate.js +0 -138
  237. package/dist/cli/rebuild.d.ts +0 -23
  238. package/dist/cli/rebuild.js +0 -22
  239. package/dist/cli/space-io.d.ts +0 -41
  240. package/dist/cli/space-io.js +0 -84
  241. package/dist/client.d.ts +0 -11
  242. package/dist/client.js +0 -54
  243. package/dist/current-space.d.ts +0 -4
  244. package/dist/current-space.js +0 -9
  245. package/dist/document-stamp.d.ts +0 -7
  246. package/dist/document-stamp.js +0 -48
  247. package/dist/image/loaders.d.ts +0 -15
  248. package/dist/image/loaders.js +0 -5
  249. package/dist/image/next-config.d.ts +0 -18
  250. package/dist/image/next-config.js +0 -32
  251. package/dist/image/remote-pattern.d.ts +0 -1
  252. package/dist/image/remote-pattern.js +0 -5
  253. package/dist/migrations.d.ts +0 -37
  254. package/dist/migrations.js +0 -73
  255. package/dist/project.d.ts +0 -88
  256. package/dist/project.js +0 -231
  257. package/dist/published.d.ts +0 -142
  258. package/dist/published.js +0 -164
  259. package/dist/ref-index.d.ts +0 -19
  260. package/dist/ref-index.js +0 -0
  261. package/dist/schema-read.d.ts +0 -23
  262. package/dist/schema-read.js +0 -50
  263. package/dist/schema-version.d.ts +0 -19
  264. package/dist/schema-version.js +0 -34
  265. package/dist/sql-space.d.ts +0 -44
  266. package/dist/sql-space.js +0 -289
  267. package/dist/sql.d.ts +0 -15
  268. package/dist/sql.js +0 -556
  269. package/dist/supabase-asset-store.d.ts +0 -21
  270. package/dist/supabase-asset-store.js +0 -170
  271. package/dist/supabase-entry-store.d.ts +0 -92
  272. package/dist/supabase-entry-store.js +0 -824
  273. package/dist/supabase-member-store.d.ts +0 -21
  274. package/dist/supabase-member-store.js +0 -96
  275. package/dist/supabase-path-index.d.ts +0 -30
  276. package/dist/supabase-path-index.js +0 -138
  277. package/dist/supabase-published-store.d.ts +0 -56
  278. package/dist/supabase-published-store.js +0 -314
  279. package/dist/supabase-store.d.ts +0 -103
  280. package/dist/supabase-store.js +0 -678
  281. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  282. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  283. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  284. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  285. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  286. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  287. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  288. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  289. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  290. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  291. package/migrations/20261009120000_smoodly_v11.sql +0 -49
  292. /package/dist/{admin/auth-config.js → deploy-store.js} +0 -0
@@ -2,8 +2,8 @@ import type { PageRecord } from "../store.ts";
2
2
  import type { PageTree } from "../page-context.ts";
3
3
  import type { EntryRecord, EntryUsage } from "../entry-store.ts";
4
4
  import type { AssetRecord, AssetMeta } from "../media.ts";
5
- import type { AssetPage, AssetQuery, FinishInput, UploadTicket } from "../asset-store.ts";
6
- import type { InviteInput, InviteResult, MemberRecord } from "../member-store.ts";
5
+ import type { AssetPage, AssetQuery } from "../asset-store.ts";
6
+ import type { MemberRecord } from "../member-store.ts";
7
7
  import type { MemberRole } from "../permissions.ts";
8
8
  import type { FieldError } from "./validate.ts";
9
9
  import type { DeployState } from "../deploy-state.ts";
@@ -11,9 +11,11 @@ import type { DraftToken } from "../draft-token.ts";
11
11
  import type { PathTarget } from "../paths.ts";
12
12
  import type { Finding } from "./drift.ts";
13
13
  import type { HealthReport } from "./health.ts";
14
+ import type { DeployStore } from "../deploy-store.ts";
14
15
  export type OpError = {
15
16
  ok: false;
16
- code: "not_found" | "conflict" | "blocked" | "validation" | "internal" | "unauthorized";
17
+ /** `no_schema`: the space has no registry yet — its site has not pushed one (spec 2026-10-16 the control plane §4). */
18
+ code: "not_found" | "conflict" | "blocked" | "validation" | "internal" | "unauthorized" | "no_schema";
17
19
  message: string;
18
20
  fields?: FieldError[];
19
21
  /** A `conflict` from the token guard (spec 2026-09-26 concurrency §2): the draft moved since the client loaded it. */
@@ -28,6 +30,15 @@ export type OpResult<T> = {
28
30
  ok: true;
29
31
  data: T;
30
32
  } | OpError;
33
+ /** A draft-cookie grant: the service signs it under the site's secret key (purpose "preview", body the
34
+ * action), the browser posts it to the site's preview route, which sets the cookie (spec 2026-10-14 §1). */
35
+ /** The service's signature over the preview action, for the site to verify (signed-message.ts), and the space it names. */
36
+ export type PreviewGrant = {
37
+ ts: number;
38
+ kid: string;
39
+ sig: string;
40
+ space: string;
41
+ };
31
42
  export type PageCreateInput = {
32
43
  parentId: string | null;
33
44
  /** The one locale the page is created in; others are added with addLocale. */
@@ -115,16 +126,11 @@ export type AssetUsageItem = {
115
126
  title: string;
116
127
  href: string | null;
117
128
  };
118
- /** The Members page's read (spec 2026-09-25 §4): the rows and, when the control plane manages them, where. */
129
+ /** The Members page's read (spec 2026-09-25 §4): the rows; the dashboard manages them. */
119
130
  export type MembersListResult = {
120
131
  members: MemberRecord[];
121
- consoleUrl: string | null;
122
132
  };
123
133
  /** `database` null = no smoodly_meta table (older than v1). Behind blocks the shell; ahead is silent (spec 2026-09-26 §5). */
124
- export type SchemaStatus = {
125
- database: number | null;
126
- required: number;
127
- };
128
134
  /** The Deploy tile's state (spec 2026-10-09 static mode §6); `hook` false = SMOODLY_DEPLOY_HOOK is unset, Deploy disabled. */
129
135
  export type DeployStatus = DeployState & {
130
136
  hook: boolean;
@@ -241,32 +247,19 @@ export type AdminOps = {
241
247
  /** Ready assets, newest first, paged by cursor; `kind` and `search` filter. */
242
248
  list(query?: AssetQuery): Promise<OpResult<AssetPage>>;
243
249
  get(id: string): Promise<OpResult<AssetRecord>>;
244
- /** Validates type and size against config.media, inserts a pending row, mints the signed upload URL (spec 2026-09-08 §6). */
245
- beginUpload(input: {
246
- name: string;
247
- mime: string;
248
- size: number;
249
- }): Promise<OpResult<UploadTicket>>;
250
- /** Marks the row ready with what the browser measured. */
251
- finishUpload(id: string, dims: FinishInput): Promise<OpResult<AssetRecord>>;
252
250
  update(id: string, meta: AssetMeta): Promise<OpResult<AssetRecord>>;
253
251
  /** Blocked while any draft or published version references the asset. */
254
252
  delete(id: string): Promise<OpResult<null>>;
255
253
  usage(id: string): Promise<OpResult<AssetUsageItem[]>>;
256
254
  };
257
255
  members: {
258
- /** The space's members; admins only. `consoleUrl` set means smoodly.io manages them and every write below is blocked. */
256
+ /** The space's members, a read-only list; admins only. Invites, roles and removals are the dashboard's
257
+ * (spec 2026-10-16 sign-in and the dashboard §6). */
259
258
  list(): Promise<OpResult<MembersListResult>>;
260
259
  /** The caller's role; every member. The shell hides Settings from editors with it. */
261
260
  me(): Promise<OpResult<{
262
261
  role: MemberRole;
263
262
  }>>;
264
- /** Admins only, self-host only: invites (or adds an existing account), refuses a duplicate row (spec 2026-09-25 members management §3). */
265
- invite(input: InviteInput): Promise<OpResult<InviteResult>>;
266
- /** Admins only, self-host only; never your own row, never the last admin. */
267
- setRole(userId: string, role: MemberRole): Promise<OpResult<MemberRecord>>;
268
- /** Admins only, self-host only; the same two guards. The Auth account stays. */
269
- remove(userId: string): Promise<OpResult<null>>;
270
263
  };
271
264
  /** Redirects by hand (spec 2026-09-26 redirects §5): open to every member, as renaming is; nothing here expires a cache. */
272
265
  redirects: {
@@ -284,12 +277,9 @@ export type AdminOps = {
284
277
  /** Every page, entry and global item against the registry, draft and live; unregistered collections counted. */
285
278
  scan(): Promise<OpResult<HealthReport>>;
286
279
  };
287
- schema: {
288
- /** Every member. The shell calls it once on mount and blocks when behind. */
289
- status(): Promise<OpResult<SchemaStatus>>;
290
- };
280
+ /** enable answers the grant the service signed, which the browser posts to the site's preview route. */
291
281
  preview: {
292
- enable(): Promise<OpResult<null>>;
282
+ enable(): Promise<OpResult<PreviewGrant | null>>;
293
283
  disable(): Promise<OpResult<null>>;
294
284
  };
295
285
  /** Static mode's Deploy tile (spec 2026-10-09 static mode §5–§7); on a live site both answer not_found. */
@@ -299,12 +289,30 @@ export type AdminOps = {
299
289
  trigger(): Promise<OpResult<DeployStatus>>;
300
290
  };
301
291
  };
302
- /** `token`: the editor's Supabase Auth session token, attached by the
303
- * client facade, sent as the Authorization header and verified by the
304
- * server gate (admin/auth.ts). */
292
+ /** `token`: the editor's pass for this space, attached by the client facade, sent as the Authorization header and
293
+ * verified by the service (spec 2026-10-16 sign-in and the dashboard §2). */
305
294
  export type AdminOpCall = {
306
295
  path: string;
307
296
  args: unknown[];
308
297
  token?: string;
309
298
  };
310
299
  export type AdminOpAction = (call: AdminOpCall) => Promise<OpResult<unknown>>;
300
+ export type AdminEffects = {
301
+ /** Expire one cache tag (`pageTag(id)` / `entryTag(id)`). */
302
+ revalidate?(tag: string): void;
303
+ /** enable may answer a grant (the service's effect); a site's own effect sets the cookie and answers nothing. */
304
+ draft?: {
305
+ enable(): Promise<PreviewGrant | void> | PreviewGrant | void;
306
+ disable(): void | Promise<void>;
307
+ };
308
+ };
309
+ /** What the deploy operations need (spec 2026-10-09 static mode §7): the live site's origin, where the marker is
310
+ * read from; the host's hook from SMOODLY_DEPLOY_HOOK (absent = Deploy disabled); the deploy store's four facts. */
311
+ export type DeployDeps = {
312
+ siteUrl: string;
313
+ hook?: string;
314
+ store: DeployStore;
315
+ /** Test seams. */
316
+ fetch?: typeof fetch;
317
+ now?: () => number;
318
+ };
@@ -1,23 +1,31 @@
1
1
  import type { PresenceConfig } from "../env.ts";
2
2
  import type { AdminRegistry } from "./serialize.ts";
3
- import type { AuthConfig } from "./auth-config.ts";
3
+ import type { SignInConfig } from "./session-flow.ts";
4
4
  export type AdminPageData = {
5
5
  registry: AdminRegistry;
6
- /** What the browser needs to sign in: public values only. */
7
- auth: AuthConfig;
8
- /** What the browser needs to join the editing-presence channel; null = presence is off. */
6
+ /** Where the browser sends a person to sign in and the dashboard it links to: public values only. */
7
+ signIn: SignInConfig;
8
+ /** Smoodly's API, without a trailing slash, and the site's space: the admin's session routes live under it. */
9
+ apiUrl: string;
10
+ spaceId: string;
11
+ /** Where the browser opens the editing-presence socket; null = presence is off. */
9
12
  presence: PresenceConfig | null;
10
- /** Where operations are posted. */
13
+ /** Where operations are posted: the service's admin API. */
11
14
  opUrl: string;
15
+ /** The site's own route the browser posts a preview grant to, which sets the draft cookie. */
16
+ previewUrl: string;
12
17
  /** The folder the package's files are served from, without a trailing slash. */
13
18
  filesBase: string;
14
19
  /** What the editor puts before a page's address to load its draft: "" where the framework serves drafts
15
20
  * at the page's own address (Next), the preview address's base where it does not (Astro). */
16
21
  previewBase: string;
22
+ /** How the canvas and History's frame mark what they want of a draft render: in the query, or in the path's
23
+ * last segment where the host strips the query (Next: Vercel keeps only a prerendered route's own params). */
24
+ canvasMarks: "path" | "query";
17
25
  /** Static mode shows the Deploy tile (spec 2026-10-09 static mode §6); live by default. */
18
26
  delivery: "live" | "static";
19
27
  };
20
28
  export declare const ADMIN_DATA_ID = "smoodly-admin-data";
21
29
  export declare const ADMIN_ROOT_ID = "smoodly-admin-root";
22
- /** The reserved segment under the admin path: the endpoint and the files live here. */
30
+ /** The reserved segment under the admin path: the files and the revalidate and preview routes live here. */
23
31
  export declare const ADMIN_INTERNAL = "_smoodly";
@@ -1,4 +1,4 @@
1
1
  export const ADMIN_DATA_ID = "smoodly-admin-data";
2
2
  export const ADMIN_ROOT_ID = "smoodly-admin-root";
3
- /** The reserved segment under the admin path: the endpoint and the files live here. */
3
+ /** The reserved segment under the admin path: the files and the revalidate and preview routes live here. */
4
4
  export const ADMIN_INTERNAL = "_smoodly";
@@ -0,0 +1,65 @@
1
+ import type { MemberRole } from "../permissions.ts";
2
+ /** Where the admin sends a person to sign in, and the dashboard its account menu and Members page link to (spec
3
+ * 2026-10-16 sign-in and the dashboard §1, §6): `GET /v1/auth` answers it from the Worker's env; the handler and the
4
+ * page data carry it. Public values only. */
5
+ export type SignInConfig = {
6
+ signInUrl: string;
7
+ consoleUrl: string;
8
+ };
9
+ export declare const STORAGE_KEY: (space: string) => string;
10
+ /** The PKCE verifier, in this tab's `sessionStorage` only: the code is good for the browser tab that asked for it. */
11
+ export declare const VERIFIER_KEY = "smoodly-verifier";
12
+ /** Renew this long before the pass expires (spec §2). */
13
+ export declare const RENEW_BEFORE_MS = 300000;
14
+ export type StoredSession = {
15
+ pass: string;
16
+ refresh: string;
17
+ expiresAt: number;
18
+ user: {
19
+ userId: string;
20
+ email: string | null;
21
+ role: MemberRole;
22
+ };
23
+ };
24
+ /** Where the session lives (`localStorage`, shared by the site's tabs) and where the verifier waits (`sessionStorage`). */
25
+ export type SessionStores = {
26
+ local: Storage;
27
+ tab: Storage;
28
+ };
29
+ /** The session the site's tabs share, or null (none, or a value that is not one). */
30
+ export declare function readSession(local: Storage, space: string): StoredSession | null;
31
+ /** Where to send the person to sign in: a fresh verifier kept in this tab, its S256 as the challenge. */
32
+ export declare function signInAddress(config: SignInConfig, space: string, returnUrl: string, stores: Pick<SessionStores, "tab">): Promise<string>;
33
+ /** The one-time code the sign-in page put in the fragment, or null. */
34
+ export declare function codeFrom(hash: string): string | null;
35
+ /** What the admin shows when the service cannot be reached at sign-in: a refusal with a way back, never a blank screen. */
36
+ export declare const UNREACHABLE = "smoodly: couldn't reach Smoodly to sign you in \u2014 check the connection and sign in again.";
37
+ /** The code and this tab's verifier for a session, stored; or why not. The verifier is spent either way. A service
38
+ * that cannot be reached is a refusal too (UNREACHABLE), so the shell shows it rather than waiting forever. */
39
+ export declare function exchange(api: string, space: string, code: string, stores: SessionStores, doFetch?: typeof fetch): Promise<StoredSession | {
40
+ refused: string;
41
+ }>;
42
+ /** The load-time step with no code in the fragment: the stored session adopted as it is while its pass has more than
43
+ * RENEW_BEFORE_MS left, else renewed; a renewal the service refuses sends the person to sign in; one that cannot reach
44
+ * the service keeps a pass that is still valid (the next timer tries again) and refuses with UNREACHABLE only once the
45
+ * pass has expired. No stored session: sign in. */
46
+ export declare function resume(api: string, space: string, stores: Pick<SessionStores, "local">, now: number, doFetch?: typeof fetch, lock?: TabLock): Promise<{
47
+ kind: "adopt";
48
+ session: StoredSession;
49
+ } | {
50
+ kind: "sign_in";
51
+ } | {
52
+ kind: "refused";
53
+ message: string;
54
+ }>;
55
+ /** Runs `fn` while holding a lock no other tab of this origin holds (Web Locks); with no Web Locks (an insecure
56
+ * context), at once. */
57
+ export type TabLock = <T>(name: string, fn: () => Promise<T>) => Promise<T>;
58
+ /** A renewed session, or null when the space refused the refresh (the stored session is then cleared). `held` is the
59
+ * refresh this tab holds: when the stored one differs, another tab renewed first and its session is adopted rather
60
+ * than posting a refresh that is already spent. The read and the post run under one lock per space across the site's
61
+ * tabs, so the tab that renews writes before the others read (spec §2), even when every tab's timer fires at once
62
+ * after a laptop wakes. A network failure throws and keeps the session. */
63
+ export declare function renew(api: string, space: string, stores: Pick<SessionStores, "local">, held: string | null, doFetch?: typeof fetch, lock?: TabLock): Promise<StoredSession | null>;
64
+ /** Sign out of this site: the space ends the session, and this browser forgets it even when the call fails. */
65
+ export declare function endSession(api: string, space: string, stores: Pick<SessionStores, "local">, doFetch?: typeof fetch): Promise<void>;
@@ -0,0 +1,121 @@
1
+ export const STORAGE_KEY = (space) => `smoodly-session:${space}`;
2
+ /** The PKCE verifier, in this tab's `sessionStorage` only: the code is good for the browser tab that asked for it. */
3
+ export const VERIFIER_KEY = "smoodly-verifier";
4
+ /** Renew this long before the pass expires (spec §2). */
5
+ export const RENEW_BEFORE_MS = 300000;
6
+ const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7
+ const s256 = async (verifier) => base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
8
+ const sessionUrl = (api, space, path) => `${api.replace(/\/+$/, "")}/v1/spaces/${encodeURIComponent(space)}/admin/${path}`;
9
+ const isSession = (v) => {
10
+ const s = v;
11
+ return typeof s === "object" && s !== null && typeof s.pass === "string" && typeof s.refresh === "string" && typeof s.expiresAt === "number"
12
+ && typeof s.user === "object" && s.user !== null && typeof s.user.userId === "string";
13
+ };
14
+ /** The session the site's tabs share, or null (none, or a value that is not one). */
15
+ export function readSession(local, space) {
16
+ try {
17
+ const value = JSON.parse(local.getItem(STORAGE_KEY(space)) ?? "null");
18
+ return isSession(value) ? value : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ const writeSession = (local, space, s) => local.setItem(STORAGE_KEY(space), JSON.stringify(s));
25
+ /** Where to send the person to sign in: a fresh verifier kept in this tab, its S256 as the challenge. */
26
+ export async function signInAddress(config, space, returnUrl, stores) {
27
+ const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
28
+ stores.tab.setItem(VERIFIER_KEY, verifier);
29
+ const query = new URLSearchParams({ space, return: returnUrl, challenge: await s256(verifier) });
30
+ return `${config.signInUrl}?${query}`;
31
+ }
32
+ /** The one-time code the sign-in page put in the fragment, or null. */
33
+ export function codeFrom(hash) {
34
+ return new URLSearchParams(hash.replace(/^#/, "")).get("smoodly_code") || null;
35
+ }
36
+ async function post(doFetch, url, body) {
37
+ const res = await doFetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
38
+ const json = (await res.json().catch(() => null));
39
+ return { status: res.status, data: json?.ok ? json.data : null, message: typeof json?.message === "string" ? json.message : null };
40
+ }
41
+ /** What the admin shows when the service cannot be reached at sign-in: a refusal with a way back, never a blank screen. */
42
+ export const UNREACHABLE = "smoodly: couldn't reach Smoodly to sign you in — check the connection and sign in again.";
43
+ /** The code and this tab's verifier for a session, stored; or why not. The verifier is spent either way. A service
44
+ * that cannot be reached is a refusal too (UNREACHABLE), so the shell shows it rather than waiting forever. */
45
+ export async function exchange(api, space, code, stores, doFetch = fetch) {
46
+ const verifier = stores.tab.getItem(VERIFIER_KEY);
47
+ stores.tab.removeItem(VERIFIER_KEY);
48
+ if (!verifier)
49
+ return { refused: "smoodly: this sign-in was started in another tab or has already been used — sign in again." };
50
+ let answer;
51
+ try {
52
+ answer = await post(doFetch, sessionUrl(api, space, "session"), { code, verifier });
53
+ }
54
+ catch {
55
+ return { refused: UNREACHABLE };
56
+ }
57
+ if (answer.status !== 200 || !isSession(answer.data))
58
+ return { refused: answer.message ?? `smoodly: the sign-in was refused (${answer.status}).` };
59
+ writeSession(stores.local, space, answer.data);
60
+ return answer.data;
61
+ }
62
+ /** The load-time step with no code in the fragment: the stored session adopted as it is while its pass has more than
63
+ * RENEW_BEFORE_MS left, else renewed; a renewal the service refuses sends the person to sign in; one that cannot reach
64
+ * the service keeps a pass that is still valid (the next timer tries again) and refuses with UNREACHABLE only once the
65
+ * pass has expired. No stored session: sign in. */
66
+ export async function resume(api, space, stores, now, doFetch = fetch, lock = tabLock) {
67
+ const stored = readSession(stores.local, space);
68
+ if (!stored)
69
+ return { kind: "sign_in" };
70
+ if (stored.expiresAt - now > RENEW_BEFORE_MS)
71
+ return { kind: "adopt", session: stored };
72
+ try {
73
+ const renewed = await renew(api, space, stores, stored.refresh, doFetch, lock);
74
+ return renewed ? { kind: "adopt", session: renewed } : { kind: "sign_in" };
75
+ }
76
+ catch {
77
+ return stored.expiresAt > now ? { kind: "adopt", session: stored } : { kind: "refused", message: UNREACHABLE };
78
+ }
79
+ }
80
+ const tabLock = (name, fn) => {
81
+ const locks = globalThis.navigator?.locks;
82
+ return locks ? locks.request(name, fn) : fn();
83
+ };
84
+ /** A renewed session, or null when the space refused the refresh (the stored session is then cleared). `held` is the
85
+ * refresh this tab holds: when the stored one differs, another tab renewed first and its session is adopted rather
86
+ * than posting a refresh that is already spent. The read and the post run under one lock per space across the site's
87
+ * tabs, so the tab that renews writes before the others read (spec §2), even when every tab's timer fires at once
88
+ * after a laptop wakes. A network failure throws and keeps the session. */
89
+ export function renew(api, space, stores, held, doFetch = fetch, lock = tabLock) {
90
+ return lock(`smoodly-renew:${space}`, () => renewHeld(api, space, stores, held, doFetch));
91
+ }
92
+ async function renewHeld(api, space, stores, held, doFetch) {
93
+ const stored = readSession(stores.local, space);
94
+ if (!stored)
95
+ return null;
96
+ if (held !== null && stored.refresh !== held)
97
+ return stored;
98
+ const answer = await post(doFetch, sessionUrl(api, space, "session/refresh"), { refresh: stored.refresh });
99
+ if (answer.status === 200 && isSession(answer.data)) {
100
+ writeSession(stores.local, space, answer.data);
101
+ return answer.data;
102
+ }
103
+ if (answer.status === 401 || answer.status === 403) {
104
+ // Cleared only if no other tab stored a newer one meanwhile.
105
+ if (readSession(stores.local, space)?.refresh === stored.refresh)
106
+ stores.local.removeItem(STORAGE_KEY(space));
107
+ return null;
108
+ }
109
+ throw new Error(answer.message ?? `smoodly: the session could not be renewed (${answer.status}).`);
110
+ }
111
+ /** Sign out of this site: the space ends the session, and this browser forgets it even when the call fails. */
112
+ export async function endSession(api, space, stores, doFetch = fetch) {
113
+ const stored = readSession(stores.local, space);
114
+ stores.local.removeItem(STORAGE_KEY(space));
115
+ if (!stored)
116
+ return;
117
+ try {
118
+ await post(doFetch, sessionUrl(api, space, "session/end"), { refresh: stored.refresh });
119
+ }
120
+ catch { /* the session lapses on its own */ }
121
+ }
@@ -1,10 +1,15 @@
1
1
  import type { AdminOpAction } from "../ops.ts";
2
2
  import type { AdminRegistry } from "../serialize.ts";
3
- import { type AuthConfig } from "./session.ts";
3
+ import type { SignInConfig } from "../session-flow.ts";
4
4
  import type { PresenceConfig } from "../../env.ts";
5
- export declare function AdminApp({ registry, op, auth, presenceConfig }: {
5
+ export declare function AdminApp({ registry, op, signIn, api, presenceConfig }: {
6
6
  registry: AdminRegistry;
7
7
  op: AdminOpAction;
8
- auth: AuthConfig;
8
+ signIn: SignInConfig;
9
+ /** Smoodly's API and the site's space: the session routes live under it. */
10
+ api: {
11
+ apiUrl: string;
12
+ spaceId: string;
13
+ };
9
14
  presenceConfig?: PresenceConfig;
10
15
  }): import("react").JSX.Element;
@@ -20,9 +20,8 @@ import { EntryForm } from "./EntryForm.js";
20
20
  import { GlobalList } from "./GlobalList.js";
21
21
  import { GlobalForm } from "./GlobalForm.js";
22
22
  import { EditorView } from "../editor/EditorView.js";
23
- import { LoginView } from "./LoginView.js";
24
- import { SetPasswordView } from "./SetPasswordView.js";
25
- import { useAdminSession } from "./session.js";
23
+ import { SignedOutView } from "./SignedOutView.js";
24
+ import { useSpaceSession } from "./session.js";
26
25
  import { IconRail } from "./IconRail.js";
27
26
  import { ConfirmHost } from "../ui/ConfirmDialog.js";
28
27
  import { notify } from "../ui/confirm.js";
@@ -33,23 +32,27 @@ import { ErrorPane } from "./ErrorPane.js";
33
32
  import { MediaScreen } from "./MediaScreen.js";
34
33
  import { SettingsView } from "./SettingsView.js";
35
34
  import { AssetsProvider } from "../forms/assets-context.js";
35
+ import { xhrSend } from "../forms/upload.js";
36
36
  import { ElementsProvider } from "../forms/elements-context.js";
37
37
  import { TimeZoneProvider } from "../forms/time-zone-context.js";
38
- import { SchemaNotice } from "./SchemaNotice.js";
39
- import { schemaBehind } from "../../schema-version.js";
40
38
  import { delivery, onPublished } from "../delivery-mode.js";
41
39
  import { pollEvery } from "./deploy-tile.js";
42
- import { createPresence, followTokenRefresh } from "./presence.js";
43
- import { connectRealtime } from "./presence-realtime.js";
40
+ import { createPresence } from "./presence.js";
41
+ import { connectSocket } from "./presence-socket.js";
44
42
  import { PresenceProvider } from "./presence-context.js";
45
- export function AdminApp({ registry, op, auth, presenceConfig }) {
46
- const session = useAdminSession(auth);
47
- // An expired session signs out here, locally: the user it names may no longer exist to log out.
48
- const [ops] = useState(() => makeClientOps(op, session.token, () => { void session.client.auth.signOut({ scope: "local" }); }));
43
+ export function AdminApp({ registry, op, signIn, api, presenceConfig }) {
44
+ const session = useSpaceSession({ apiUrl: api.apiUrl, spaceId: api.spaceId, signIn });
45
+ // The session's getters read refs, so the facade made once always sends the current pass; a refused pass renews
46
+ // once and the call is sent again, or the person is sent to sign in.
47
+ const renewRef = useRef(session.renew);
48
+ renewRef.current = session.renew;
49
+ const [ops] = useState(() => makeClientOps(op, session.token, () => renewRef.current()));
50
+ // Uploads go to the service's media route with the same pass and the same renewal (spec 2026-10-18 media §4).
51
+ const [upload] = useState(() => ({ send: xhrSend(`${api.apiUrl.replace(/\/+$/, "")}/v1/spaces/${encodeURIComponent(api.spaceId)}/admin/media`, session.token, () => renewRef.current()) }));
49
52
  const route = useRoute(registry.adminPath);
50
53
  const [head = "pages", tail] = route.segments.length ? route.segments : ["pages"];
51
54
  const isCollection = registry.collections.some((c) => c.name === head);
52
- const email = session.state.status === "signed_in" ? session.state.session.user.email : undefined;
55
+ const email = session.state.status === "signed_in" ? session.state.user.email ?? undefined : undefined;
53
56
  // The role rides on members.me (members management §2): Settings hides
54
57
  // from an editor, and the area itself refuses. Unknown until it answers.
55
58
  const signedIn = session.state.status === "signed_in";
@@ -67,28 +70,13 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
67
70
  setRole("editor"); });
68
71
  return () => { live = false; };
69
72
  }, [signedIn, ops]);
70
- // The one version check (spec 2026-09-26 §5): behind blocks the shell,
71
- // equal or ahead renders as usual. Unknown until it answers; a failed
72
- // call renders as usual too — the notice is for a known-behind schema.
73
- const [schema, setSchema] = useState(null);
74
- useEffect(() => {
75
- if (!signedIn) {
76
- setSchema(null);
77
- return;
78
- }
79
- let live = true;
80
- ops.schema.status()
81
- .then((r) => { if (live && r.ok)
82
- setSchema(r.data); })
83
- .catch(() => { });
84
- return () => { live = false; };
85
- }, [signedIn, ops]);
86
73
  // The Deploy tile (spec 2026-10-09 static mode §6): on a static site, deploy.status every 30 s, every 5 s
87
74
  // while a build is in flight, and at once after any publish; the press records and re-reads.
88
75
  const [deploy, setDeploy] = useState(null);
89
76
  const [deployBusy, setDeployBusy] = useState(false);
90
- const [publishes, setPublishes] = useState(0);
91
- useEffect(() => onPublished(() => setPublishes((n) => n + 1)), []);
77
+ // A publish is done when its pointer moved (spec 2026-10-15 the space object §3): the tile re-reads at once.
78
+ const [deployTick, setDeployTick] = useState(0);
79
+ useEffect(() => onPublished(() => setDeployTick((n) => n + 1)), []);
92
80
  useEffect(() => {
93
81
  if (!signedIn || delivery() !== "static")
94
82
  return;
@@ -112,7 +100,7 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
112
100
  void tick();
113
101
  return () => { live = false; if (timer)
114
102
  clearTimeout(timer); };
115
- }, [signedIn, ops, publishes]);
103
+ }, [signedIn, ops, deployTick]);
116
104
  const onDeploy = async () => {
117
105
  setDeployBusy(true);
118
106
  try {
@@ -126,37 +114,31 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
126
114
  setDeployBusy(false);
127
115
  }
128
116
  };
129
- const userId = session.state.status === "signed_in" ? session.state.session.user.id : "";
130
- // Presence (spec 2026-09-26 concurrency §5; gated 2026-09-26 security pass §1):
131
- // one channel per signed-in session, the content project's public config as the
132
- // gateway and the SESSION token on the socket; absent config = presence off.
133
- // The token getter is a new closure every render — read it through a ref so
134
- // the effect keys on the session, not on the render.
117
+ // Presence (spec 2026-09-26 concurrency §5; spec 2026-10-16 sign-in and the dashboard §3): one socket per signed-in
118
+ // tab to the service, opened with a one-time ticket asked with the current pass; every renewal reconnects with a
119
+ // fresh one. Absent config = presence off. The getters are new closures every render — read them through refs so the
120
+ // effect keys on the session, not on the render.
135
121
  const [presence, setPresence] = useState(null);
136
122
  const tokenRef = useRef(session.token);
137
123
  tokenRef.current = session.token;
138
- const authClient = session.client;
139
- const contentUrl = presenceConfig?.url, contentKey = presenceConfig?.key, contentSpace = presenceConfig?.spaceId;
124
+ const onRenew = useRef(session.onRenew);
125
+ onRenew.current = session.onRenew;
126
+ const presenceApi = presenceConfig?.apiUrl, presenceSpace = presenceConfig?.spaceId;
140
127
  useEffect(() => {
141
- if (!signedIn || !contentUrl || !contentKey || !contentSpace || !email) {
128
+ if (!signedIn || !presenceApi || !presenceSpace || !email) {
142
129
  setPresence(null);
143
130
  return;
144
131
  }
145
- const config = { url: contentUrl, key: contentKey, spaceId: contentSpace };
132
+ const config = { apiUrl: presenceApi, spaceId: presenceSpace };
146
133
  const p = createPresence({
147
134
  email,
148
- connect: () => {
149
- const token = tokenRef.current();
150
- if (!token)
151
- throw new Error("smoodly: no session token for presence");
152
- return connectRealtime(config, email, token);
153
- },
135
+ connect: () => connectSocket(config, email, { pass: () => tokenRef.current(), renew: () => renewRef.current() }),
154
136
  });
155
137
  p.join();
156
138
  setPresence(p);
157
- const stop = followTokenRefresh(authClient.auth, p);
139
+ const stop = onRenew.current((pass) => p.setAuth(pass));
158
140
  return () => { stop(); p.dispose(); setPresence(null); };
159
- }, [signedIn, contentUrl, contentKey, contentSpace, email, authClient]);
141
+ }, [signedIn, presenceApi, presenceSpace, email]);
160
142
  if (session.state.status === "loading") {
161
143
  return (_jsx("div", { style: { height: "100vh", display: "grid", placeItems: "center", color: "var(--text-muted)" }, children: "Loading\u2026" }));
162
144
  }
@@ -164,13 +146,10 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
164
146
  if (mismatch) {
165
147
  return _jsx("div", { style: { height: "100vh", display: "flex" }, children: _jsx(ErrorPane, { message: mismatch, back: false }) });
166
148
  }
167
- if (session.state.status === "signed_out")
168
- return _jsx(LoginView, { client: session.client, providers: auth.providers });
169
- // An invite or reset link signed the person in: Set password before the shell (members management §6).
170
- if (session.arrival)
171
- return _jsx(SetPasswordView, { client: session.client, mode: session.arrival, onDone: session.clearArrival });
172
- if (schema && schemaBehind(schema.database, schema.required))
173
- return _jsx(SchemaNotice, { status: schema });
149
+ if (session.state.status === "signed_out") {
150
+ const message = session.state.reason === "refused" ? session.state.message : undefined;
151
+ return _jsx(SignedOutView, { host: window.location.host, message: message, onSignIn: () => { void session.signInAgain(); } });
152
+ }
174
153
  let main;
175
154
  if (head === "pages" && !tail)
176
155
  main = _jsx(PagesList, { ops: ops, registry: registry });
@@ -185,10 +164,10 @@ export function AdminApp({ registry, op, auth, presenceConfig }) {
185
164
  else if (head === "globals" && tail)
186
165
  main = _jsx(GlobalForm, { ops: ops, registry: registry, name: decodeURIComponent(tail) });
187
166
  else if (head === "settings")
188
- main = _jsx(SettingsView, { ops: ops, registry: registry, tail: tail, role: role, userId: userId });
167
+ main = _jsx(SettingsView, { ops: ops, registry: registry, tail: tail, role: role });
189
168
  else if (head === "media")
190
169
  main = _jsx(MediaScreen, {});
191
170
  else
192
171
  main = _jsx(ErrorPane, { message: "Not found." });
193
- return (_jsx(AdminPathProvider, { value: registry.adminPath, children: _jsx(PresenceProvider, { value: presence, children: _jsx(ElementsProvider, { elements: registry.elements, children: _jsx(TimeZoneProvider, { zone: registry.timeZone, children: _jsxs(AssetsProvider, { ops: ops, media: registry.media, children: [_jsxs("div", { className: "sm-shell", children: [_jsx(IconRail, { registry: registry, active: head, email: email, settings: role !== null, deploy: deploy ? { status: deploy, busy: deployBusy, onDeploy: () => { void onDeploy(); } } : undefined, onSignOut: () => session.client.auth.signOut() }), _jsx("main", { className: "sm-main", children: main }, `${head}/${tail ?? ""}`)] }), _jsx(ConfirmHost, {})] }) }) }) }) }));
172
+ return (_jsx(AdminPathProvider, { value: registry.adminPath, children: _jsx(PresenceProvider, { value: presence, children: _jsx(ElementsProvider, { elements: registry.elements, children: _jsx(TimeZoneProvider, { zone: registry.timeZone, children: _jsxs(AssetsProvider, { ops: ops, media: registry.media, upload: upload, children: [_jsxs("div", { className: "sm-shell", children: [_jsx(IconRail, { registry: registry, active: head, email: email, settings: role !== null, deploy: deploy ? { status: deploy, busy: deployBusy, onDeploy: () => { void onDeploy(); } } : undefined, onSignOut: () => { void session.signOut(); } }), _jsx("main", { className: "sm-main", children: main }, `${head}/${tail ?? ""}`)] }), _jsx(ConfirmHost, {})] }) }) }) }) }));
194
173
  }
@@ -26,9 +26,7 @@ export function ContentHealthPage({ ops, registry, initial = null }) {
26
26
  return _jsx(ErrorPane, { message: error, back: false, onRetry: scan });
27
27
  const sections = report ? healthSections(report, registry) : null;
28
28
  const brokenCount = sections ? sections.broken.length + sections.collections.length : 0;
29
- return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx("span", { className: "sm-title", children: "Content health" }), right: _jsxs(_Fragment, { children: [report && _jsx("span", { className: "sm-hint", children: "Checked just now" }), _jsx(Button, { variant: "secondary", onClick: scan, disabled: !report, children: "Rescan" })] }) }), _jsx("div", { className: "sm-listwrap", children: _jsxs("div", { className: "sm-table sm-health", children: [!sections && _jsx("div", { className: "sm-empty", children: "Checking content\u2026" }), report?.delivery && (_jsx("p", { className: "sm-health__delivery", children: report.delivery.behind === 0
30
- ? "Every published document is current."
31
- : _jsxs(_Fragment, { children: [report.delivery.behind, " ", report.delivery.behind === 1 ? "document is" : "documents are", " behind the current stamp. Run ", _jsx("code", { children: "npx smoodly rebuild" }), " in the site's folder."] }) })), sections && brokenCount === 0 && sections.orphan.length === 0 && _jsx("div", { className: "sm-empty", children: "Everything matches the code." }), sections && brokenCount > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "sm-health__section", children: ["Blocks publishing \u00B7 ", brokenCount] }), sections.collections.map((c) => _jsx("p", { className: "sm-health__headline", children: c.headline }, c.name)), sections.broken.map((g) => _jsx(Group, { group: g }, g.key))] })), sections && sections.orphan.length > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "sm-health__section", children: ["Left over \u00B7 ", sections.orphan.length] }), sections.orphan.map((g) => _jsx(Group, { group: g }, g.key))] }))] }) })] }));
29
+ return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx("span", { className: "sm-title", children: "Content health" }), right: _jsxs(_Fragment, { children: [report && _jsx("span", { className: "sm-hint", children: "Checked just now" }), _jsx(Button, { variant: "secondary", onClick: scan, disabled: !report, children: "Rescan" })] }) }), _jsx("div", { className: "sm-listwrap", children: _jsxs("div", { className: "sm-table sm-health", children: [!sections && _jsx("div", { className: "sm-empty", children: "Checking content\u2026" }), sections && brokenCount === 0 && sections.orphan.length === 0 && _jsx("div", { className: "sm-empty", children: "Everything matches the code." }), sections && brokenCount > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "sm-health__section", children: ["Blocks publishing \u00B7 ", brokenCount] }), sections.collections.map((c) => _jsx("p", { className: "sm-health__headline", children: c.headline }, c.name)), sections.broken.map((g) => _jsx(Group, { group: g }, g.key))] })), sections && sections.orphan.length > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "sm-health__section", children: ["Left over \u00B7 ", sections.orphan.length] }), sections.orphan.map((g) => _jsx(Group, { group: g }, g.key))] }))] }) })] }));
32
30
  }
33
31
  function Group({ group }) {
34
32
  const adminHref = useAdminHref();
@@ -1,10 +1,7 @@
1
1
  import type { AdminGlobal } from "../serialize.ts";
2
2
  import type { Group } from "../forms/tabs.ts";
3
- import { type NodeValues } from "../editor/InspectorFields.tsx";
4
3
  import type { RefOption } from "../forms/ref-picker.ts";
5
4
  export type { RefOption };
6
- /** The item's value map as the inspector reads it: the reserved keys pulled out into their namespaces. */
7
- export declare function globalValues(fields: Record<string, unknown>): NodeValues;
8
5
  export declare function GlobalFields({ item, groups, fields, errors, refOptions, multilingual, inlinePath, onEditHere, onChange }: {
9
6
  item: AdminGlobal;
10
7
  groups: Group[];
@@ -5,7 +5,7 @@ import { FieldWidget } from "../forms/FieldWidget.js";
5
5
  import { InspectorFields, valueOf } from "../editor/InspectorFields.js";
6
6
  import { UnlocalizedMark } from "../ui/primitives.js";
7
7
  /** The item's value map as the inspector reads it: the reserved keys pulled out into their namespaces. */
8
- export function globalValues(fields) {
8
+ function globalValues(fields) {
9
9
  const ns = (key) => (fields[key] && typeof fields[key] === "object" ? fields[key] : {});
10
10
  return { fields, settings: ns(SETTINGS_KEY), styles: ns(STYLES_KEY), css: typeof fields[CSS_KEY] === "string" ? fields[CSS_KEY] : undefined };
11
11
  }