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
package/dist/published.js DELETED
@@ -1,164 +0,0 @@
1
- // The delivery model's store (spec 2026-10-07 delivery model §1, §5): the
2
- // finished documents Publish writes, one row per page, entry or global per
3
- // locale, read in one lookup. Derived from the editing tables and
4
- // rebuildable; the memory adapter is two maps with the matcher in code, the
5
- // Supabase one (supabase-published-store.ts) reads published_pages,
6
- // published_entries, the published_routes view and smoodly_list_published.
7
- import { matchesWhere } from "./entry-store.js";
8
- const keyOf = (id, locale) => `${id}:${locale}`;
9
- const asc = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
10
- /** smoodly_list_published's ORDER BY: manual by sort, else the built-in time or
11
- * the field, absent, null and empty last in both directions; ties by creation, then id. */
12
- function compareRows(order) {
13
- return (a, b) => {
14
- if (order === "manual") {
15
- const d = (a.sort ?? Infinity) - (b.sort ?? Infinity);
16
- if (d !== 0 && !Number.isNaN(d))
17
- return d;
18
- }
19
- else if (order) {
20
- const value = (r) => {
21
- const v = order.by === "createdAt" ? r.createdAt : order.by === "updatedAt" ? r.updatedAt : r.fields[order.by];
22
- return v === "" ? null : v;
23
- };
24
- const va = value(a), vb = value(b);
25
- if (va == null && vb != null)
26
- return 1;
27
- if (vb == null && va != null)
28
- return -1;
29
- if (va != null && vb != null) {
30
- const d = typeof va === "number" && typeof vb === "number" ? va - vb : String(va).localeCompare(String(vb));
31
- if (d !== 0)
32
- return order.direction === "asc" ? d : -d;
33
- }
34
- }
35
- return asc(a.createdAt, b.createdAt) || asc(a.id, b.id);
36
- };
37
- }
38
- /** An address holds one row per table, as the SQL's unique (space, locale, path) index says: a row of another id
39
- * left at an incoming address (a stale row a capped rename did not reach) goes before the write. */
40
- function vacate(map, rows) {
41
- const incoming = new Map(rows.filter((r) => r.path !== null).map((r) => [`${r.locale}\u0000${r.path}`, r.id]));
42
- for (const [k, row] of map) {
43
- const holder = row.path === null ? undefined : incoming.get(`${row.locale}\u0000${row.path}`);
44
- if (holder !== undefined && holder !== row.id)
45
- map.delete(k);
46
- }
47
- }
48
- export class MemoryPublishedStore {
49
- constructor() {
50
- this.pages = new Map();
51
- this.entryRows = new Map();
52
- this.recorded = null;
53
- this.deploy = null;
54
- this.removedAt = null;
55
- }
56
- async recordedModel() {
57
- return this.recorded;
58
- }
59
- async recordModel(model) {
60
- this.recorded = model;
61
- }
62
- async newestPublishedAt() {
63
- const all = [...this.pages.values(), ...this.entryRows.values()].map((r) => r.publishedAt);
64
- return all.length === 0 ? null : Math.max(...all);
65
- }
66
- async countPublishedAfter(since) {
67
- return [...this.pages.values(), ...this.entryRows.values()].filter((r) => r.publishedAt > since).length;
68
- }
69
- async deployRequest() {
70
- return this.deploy;
71
- }
72
- async requestDeploy(by, at = Date.now()) {
73
- this.deploy = { at, by };
74
- }
75
- async countBehind(model) {
76
- const rows = [...this.pages.values(), ...this.entryRows.values()];
77
- return { behind: rows.filter((r) => r.model !== model).length, total: rows.length };
78
- }
79
- async putPages(rows, options) {
80
- vacate(this.pages, rows);
81
- for (const row of rows) {
82
- const kept = options?.keep ? this.pages.get(keyOf(row.id, row.locale))?.publishedAt : undefined;
83
- this.pages.set(keyOf(row.id, row.locale), structuredClone(kept === undefined ? row : { ...row, publishedAt: kept }));
84
- }
85
- }
86
- async putEntries(rows, options) {
87
- vacate(this.entryRows, rows);
88
- for (const row of rows) {
89
- const kept = options?.keep ? this.entryRows.get(keyOf(row.id, row.locale))?.publishedAt : undefined;
90
- this.entryRows.set(keyOf(row.id, row.locale), structuredClone(kept === undefined ? row : { ...row, publishedAt: kept }));
91
- }
92
- }
93
- async remove(keys) {
94
- let deleted = 0;
95
- for (const key of keys) {
96
- const map = key.kind === "page" ? this.pages : this.entryRows;
97
- for (const [k, row] of map)
98
- if (row.id === key.id && (key.locale === undefined || row.locale === key.locale)) {
99
- map.delete(k);
100
- deleted += 1;
101
- }
102
- }
103
- // Only a row that went is a change (a key with no row is not): the Deploy tile counts it (spec 2026-10-09 §5).
104
- if (deleted > 0)
105
- this.removedAt = Date.now();
106
- }
107
- async lastRemovalAt() {
108
- return this.removedAt;
109
- }
110
- async byAddress(locale, path, globals) {
111
- const found = () => {
112
- for (const row of this.pages.values()) {
113
- if (row.locale === locale && row.path === path && row.hasPage)
114
- return { kind: "page", row: structuredClone(row) };
115
- }
116
- for (const row of this.entryRows.values()) {
117
- if (row.locale === locale && row.path === path)
118
- return { kind: "entry", row: structuredClone(row) };
119
- }
120
- return null;
121
- };
122
- const route = found();
123
- if (!route || !globals)
124
- return route;
125
- const wanted = new Set(globals);
126
- route.globals = structuredClone([...this.entryRows.values()].filter((r) => r.locale === locale && wanted.has(r.collection)));
127
- return route;
128
- }
129
- async page(id, locale) {
130
- const row = this.pages.get(keyOf(id, locale));
131
- return row ? structuredClone(row) : null;
132
- }
133
- async entry(id, locale) {
134
- const row = this.entryRows.get(keyOf(id, locale));
135
- return row ? structuredClone(row) : null;
136
- }
137
- async entries(collection, q) {
138
- const wanted = q.ids ? new Set(q.ids) : null;
139
- const rows = [...this.entryRows.values()].filter((r) => r.collection === collection &&
140
- r.locale === q.locale &&
141
- (q.parent === undefined || r.parentId === q.parent) &&
142
- (!wanted || wanted.has(r.id)) &&
143
- (!q.where || matchesWhere(r.fields, q.where)));
144
- rows.sort(compareRows(q.order));
145
- return structuredClone(typeof q.limit === "number" ? rows.slice(0, q.limit) : rows);
146
- }
147
- async tree(locale) {
148
- return [...this.pages.values()]
149
- .filter((r) => r.locale === locale)
150
- .sort((a, b) => {
151
- const d = (a.sort ?? Infinity) - (b.sort ?? Infinity);
152
- return (d !== 0 && !Number.isNaN(d) ? d : 0) || asc(a.publishedAt, b.publishedAt) || asc(a.id, b.id);
153
- })
154
- .map(({ id, parentId, sort, path, title, hasPage, linkFields, model, stale }) => structuredClone({ id, parentId, sort, path, title, hasPage, linkFields, model, stale }));
155
- }
156
- async markStale(keys) {
157
- for (const key of keys) {
158
- const map = key.kind === "page" ? this.pages : this.entryRows;
159
- for (const row of map.values())
160
- if (row.id === key.id && (key.locale === undefined || row.locale === key.locale))
161
- row.stale = true;
162
- }
163
- }
164
- }
@@ -1,19 +0,0 @@
1
- import type { RefEdge } from "./refs.ts";
2
- export type RefSource = {
3
- sourceKind: string;
4
- sourceId: string;
5
- };
6
- export declare class MemoryRefIndex {
7
- private rows;
8
- /** Replace a source's outgoing edges wholesale (what every save does). */
9
- rewrite(source: RefSource, edges: RefEdge[]): void;
10
- remove({ sourceKind, sourceId }: RefSource): void;
11
- /** The reverse question: who points at this target? */
12
- to(targetCollection: string, targetId: string): {
13
- sourceKind: string;
14
- sourceId: string;
15
- sourceField: string;
16
- }[];
17
- /** Reverse lookup by id alone (ids are globally unique) — the fan-out walk. */
18
- incoming(targetId: string): RefSource[];
19
- }
package/dist/ref-index.js DELETED
Binary file
@@ -1,23 +0,0 @@
1
- import type { SupabaseClient } from "@supabase/supabase-js";
2
- export type SchemaRead = {
3
- version: number;
4
- } | {
5
- missing: "key" | "schema";
6
- };
7
- /** The policies show `smoodly_meta` to a write key only, so no visible row means the key is not a secret key (spec §5). */
8
- export declare function readSchemaVersion(db: SupabaseClient): Promise<SchemaRead>;
9
- /** How long a refused key is believed before it is read again. */
10
- export declare const KEY_RECHECK_MS = 30000;
11
- /** The site's schema check, once per process (spec 2026-09-26 §5). An old or missing schema warns
12
- * and never blocks. A key that cannot see the version row reads every table as empty rather than
13
- * refused, so lookups fail instead of serving empty pages and 404s (decided 2026-10-02) — but only
14
- * for {@link KEY_RECHECK_MS}: a fixed key or a briefly missing row recovers without a restart. A
15
- * failed read refuses nothing and is tried again; the lookup itself names that error. */
16
- export declare class SecretKeyCheck {
17
- private readonly read;
18
- private readonly now;
19
- private pending;
20
- private refusedAt;
21
- constructor(read: () => Promise<SchemaRead>, now?: () => number);
22
- refused(): Promise<boolean>;
23
- }
@@ -1,50 +0,0 @@
1
- import { SCHEMA_VERSION_KEY, schemaBehind, schemaBehindMessage } from "./schema-version.js";
2
- /** The policies show `smoodly_meta` to a write key only, so no visible row means the key is not a secret key (spec §5). */
3
- export async function readSchemaVersion(db) {
4
- const res = await db.from("smoodly_meta").select("value").eq("key", SCHEMA_VERSION_KEY).maybeSingle();
5
- if (res.error) {
6
- // PGRST205: not in PostgREST's schema cache; 42P01: undefined table; 42501: no grant.
7
- if (res.error.code === "PGRST205" || res.error.code === "42P01")
8
- return { missing: "schema" };
9
- if (res.error.code === "42501")
10
- return { missing: "key" };
11
- throw new Error(`smoodly: reading the schema version: ${res.error.message}`);
12
- }
13
- const n = Number(res.data?.value);
14
- return res.data && Number.isInteger(n) ? { version: n } : { missing: "key" };
15
- }
16
- /** How long a refused key is believed before it is read again. */
17
- export const KEY_RECHECK_MS = 30000;
18
- /** The site's schema check, once per process (spec 2026-09-26 §5). An old or missing schema warns
19
- * and never blocks. A key that cannot see the version row reads every table as empty rather than
20
- * refused, so lookups fail instead of serving empty pages and 404s (decided 2026-10-02) — but only
21
- * for {@link KEY_RECHECK_MS}: a fixed key or a briefly missing row recovers without a restart. A
22
- * failed read refuses nothing and is tried again; the lookup itself names that error. */
23
- export class SecretKeyCheck {
24
- constructor(read, now = Date.now) {
25
- this.read = read;
26
- this.now = now;
27
- this.pending = null;
28
- this.refusedAt = null;
29
- }
30
- refused() {
31
- if (this.refusedAt !== null && this.now() - this.refusedAt >= KEY_RECHECK_MS)
32
- this.pending = null;
33
- this.pending ?? (this.pending = this.read().then((read) => {
34
- if ("missing" in read && read.missing === "key") {
35
- this.refusedAt = this.now();
36
- return true;
37
- }
38
- this.refusedAt = null;
39
- if ("missing" in read)
40
- console.warn(schemaBehindMessage(null));
41
- else if (schemaBehind(read.version))
42
- console.warn(schemaBehindMessage(read.version));
43
- return false;
44
- }, () => {
45
- this.pending = null;
46
- return false;
47
- }));
48
- return this.pending;
49
- }
50
- }
@@ -1,19 +0,0 @@
1
- /** The minimum schema version the package needs. Moves ONLY in a release
2
- * that adds a table, column, function or policy the package reads; a
3
- * unit test holds it equal to the highest file in `migrations/`. */
4
- export declare const SCHEMA_VERSION = 11;
5
- export declare const SCHEMA_VERSION_KEY = "schema_version";
6
- export declare const CORE_FILE_RE: RegExp;
7
- export declare const VIEWS_FILE_RE: RegExp;
8
- /** The version a core file leaves behind, from its name; null for anything else. */
9
- export declare function coreFileVersion(name: string): number | null;
10
- /** The last statement of every core file. */
11
- export declare function schemaVersionSQL(version: number): string;
12
- /** Behind blocks the admin and warns on the site (§5). Null = no
13
- * `smoodly_meta` table, a database older than version 1. Ahead is the
14
- * normal state of a cloud app between our migration and its package
15
- * bump, and is fine. */
16
- export declare const schemaBehind: (database: number | null, required?: number) => boolean;
17
- export declare function schemaBehindMessage(database: number | null, required?: number): string;
18
- /** The version row is hidden from anything but a secret key, so an invisible row means the wrong key, not an old schema. */
19
- export declare const secretKeyRefusedMessage = "smoodly: SMOODLY_SECRET_KEY cannot read the schema version \u2014 it is not the project's secret (service-role) key; the publishable key there is the usual mix-up.";
@@ -1,34 +0,0 @@
1
- // packages/smoodly/src/schema-version.ts
2
- // The schema version this package needs (spec 2026-09-26 §1) and the two
3
- // things every side agrees on: the `smoodly_meta` row that records a
4
- // database's version, and the names of the files the package ships. Pure
5
- // and framework-free — the SQL emitters, the CLI, the stores, the admin
6
- // op and the shell all read it from here.
7
- /** The minimum schema version the package needs. Moves ONLY in a release
8
- * that adds a table, column, function or policy the package reads; a
9
- * unit test holds it equal to the highest file in `migrations/`. */
10
- export const SCHEMA_VERSION = 11;
11
- export const SCHEMA_VERSION_KEY = "schema_version";
12
- export const CORE_FILE_RE = /^(\d{14})_smoodly_v(\d+)\.sql$/;
13
- export const VIEWS_FILE_RE = /^(\d{14})_smoodly_views\.sql$/;
14
- /** The version a core file leaves behind, from its name; null for anything else. */
15
- export function coreFileVersion(name) {
16
- const m = CORE_FILE_RE.exec(name);
17
- return m ? Number(m[2]) : null;
18
- }
19
- /** The last statement of every core file. */
20
- export function schemaVersionSQL(version) {
21
- return (`insert into "smoodly_meta" ("key", "value") values ('${SCHEMA_VERSION_KEY}', '${version}')\n` +
22
- ` on conflict ("key") do update set "value" = excluded."value";`);
23
- }
24
- /** Behind blocks the admin and warns on the site (§5). Null = no
25
- * `smoodly_meta` table, a database older than version 1. Ahead is the
26
- * normal state of a cloud app between our migration and its package
27
- * bump, and is fine. */
28
- export const schemaBehind = (database, required = SCHEMA_VERSION) => database === null || database < required;
29
- export function schemaBehindMessage(database, required = SCHEMA_VERSION) {
30
- return (`smoodly: the database schema is at version ${database ?? "none"}; this Smoodly needs version ${required}. ` +
31
- "Run `npx smoodly migrate`, then apply it: `supabase db push` (hosted) or `supabase migration up` (local).");
32
- }
33
- /** The version row is hidden from anything but a secret key, so an invisible row means the wrong key, not an old schema. */
34
- export const secretKeyRefusedMessage = "smoodly: SMOODLY_SECRET_KEY cannot read the schema version — it is not the project's secret (service-role) key; the publishable key there is the usual mix-up.";
@@ -1,44 +0,0 @@
1
- /** The one space a self-host install has; the space a request with no
2
- * claim resolves to. The bare uuid — code needs the value (a shared
3
- * item's id namespace), SQL needs it quoted. */
4
- export declare const DEFAULT_SPACE_ID = "00000000-0000-0000-0000-000000000000";
5
- export declare const DEFAULT_SPACE = "'00000000-0000-0000-0000-000000000000'";
6
- export declare const SPACE_COL = "\"space_id\" uuid not null default '00000000-0000-0000-0000-000000000000'";
7
- export declare const CONTENT_TABLES: readonly ["pages", "page_locales", "page_versions", "entries", "entry_locales", "entry_versions", "refs", "paths", "assets"];
8
- export type ContentTable = (typeof CONTENT_TABLES)[number];
9
- /** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
10
- * space-stamped like content, but not transferred (export skips them, import
11
- * rebuilds them) and not counted toward the row cap, since they are derived.
12
- * `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current;
13
- * `space_deploys` (schema v11) one row per space: the last Deploy press of static mode. */
14
- export declare const DELIVERY_TABLES: readonly ["published_pages", "published_entries", "published_models", "space_deploys"];
15
- export declare function currentSpaceSQL(): string;
16
- /** One permissive policy per action (spec 2026-09-29 pre-install fixes §3):
17
- * the write key's insert, update and delete, each alone for its action; its
18
- * reads go through the table's one SELECT policy. `table` is as SQL names
19
- * it (`"pages"`, `storage.objects`). Drops the old `for all` policy first. */
20
- export declare function writePoliciesSQL(table: string, write: string, prefix: string): string;
21
- /** The content tables' and members' policies. Each content table has ONE
22
- * select policy — its own space, not revoked, and a write key (spec
23
- * 2026-09-29 content API contract §8): no key but a write key reads
24
- * content; the publishable key serves sign-in and presence. Split write
25
- * policies beside it. `refs` and `members` keep their `for all` write
26
- * policy, which is also their only select. */
27
- export declare function contentPoliciesSQL(): string;
28
- export declare function spaceLayerSQL(): string;
29
- /** The media bucket and its object policies (spec 2026-09-08 media §3,
30
- * §12). Public read: the CDN and next/image fetch without tokens. Writes:
31
- * a write key under its own space's prefix only; the service role
32
- * bypasses; signed uploads are minted server-side with that same key.
33
- * Schema-qualified calls: storage policies do not run with public on
34
- * the search path. Last in the migration, after the functions exist. */
35
- export declare function storageSQL(): string;
36
- /** The media bucket's write policies: insert, update, delete for a write
37
- * key under its own space's prefix; reads are the public select above. */
38
- export declare function mediaPoliciesSQL(): string;
39
- /** The three mutation RPCs are the stores' verbs, never a browser's (spec
40
- * 2026-09-26 security pass §2): executable by the service role and by
41
- * `authenticated` (the role a cloud write key carries), not by anon or
42
- * public. `smoodly_current_space` is NOT here — the stores read it through
43
- * rpc(). Last in the schema, after every definition exists. */
44
- export declare function grantsSQL(): string;
package/dist/sql-space.js DELETED
@@ -1,289 +0,0 @@
1
- // The space layer of the stable schema (spec 2026-09-04 §2–§3). A space
2
- // is one cloud environment; a self-host install is one space with the
3
- // default id. Everything that needs "which space is this request in"
4
- // reads smoodly_current_space() — the adapters never mention spaces.
5
- /** The one space a self-host install has; the space a request with no
6
- * claim resolves to. The bare uuid — code needs the value (a shared
7
- * item's id namespace), SQL needs it quoted. */
8
- export const DEFAULT_SPACE_ID = "00000000-0000-0000-0000-000000000000";
9
- export const DEFAULT_SPACE = `'${DEFAULT_SPACE_ID}'`;
10
- export const SPACE_COL = `"space_id" uuid not null default ${DEFAULT_SPACE}`;
11
- export const CONTENT_TABLES = [
12
- "pages",
13
- "page_locales",
14
- "page_versions",
15
- "entries",
16
- "entry_locales",
17
- "entry_versions",
18
- "refs",
19
- "paths",
20
- "assets",
21
- ];
22
- /** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
23
- * space-stamped like content, but not transferred (export skips them, import
24
- * rebuilds them) and not counted toward the row cap, since they are derived.
25
- * `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current;
26
- * `space_deploys` (schema v11) one row per space: the last Deploy press of static mode. */
27
- export const DELIVERY_TABLES = ["published_pages", "published_entries", "published_models", "space_deploys"];
28
- export function currentSpaceSQL() {
29
- return `
30
- -- The space a request acts in: the key's claim in Smoodly Cloud, the
31
- -- default space in self-host (service role, anon key). Policies, the
32
- -- insert trigger and the reorder RPC all read this ONE function.
33
- create or replace function "smoodly_current_space"() returns uuid
34
- language sql stable set search_path = '' as $$
35
- select coalesce((auth.jwt() ->> 'space_id')::uuid, ${DEFAULT_SPACE});
36
- $$;`.trim();
37
- }
38
- /** Tables whose existing indexes do not lead with space_id. */
39
- const NEEDS_SPACE_INDEX = [
40
- "page_locales", "page_versions", "entry_locales", "entry_versions", "refs",
41
- ];
42
- /** One permissive policy per action (spec 2026-09-29 pre-install fixes §3):
43
- * the write key's insert, update and delete, each alone for its action; its
44
- * reads go through the table's one SELECT policy. `table` is as SQL names
45
- * it (`"pages"`, `storage.objects`). Drops the old `for all` policy first. */
46
- export function writePoliciesSQL(table, write, prefix) {
47
- return [
48
- `drop policy if exists "${prefix}_write" on ${table};`,
49
- `drop policy if exists "${prefix}_insert" on ${table};\ncreate policy "${prefix}_insert" on ${table} for insert\n with check (${write});`,
50
- `drop policy if exists "${prefix}_update" on ${table};\ncreate policy "${prefix}_update" on ${table} for update\n using (${write})\n with check (${write});`,
51
- `drop policy if exists "${prefix}_delete" on ${table};\ncreate policy "${prefix}_delete" on ${table} for delete\n using (${write});`,
52
- ].join("\n\n");
53
- }
54
- const OWN = `"space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"())`;
55
- const WRITE = `${OWN} and (select "smoodly_key_kind"()) = 'write'`;
56
- /** The content tables' and members' policies. Each content table has ONE
57
- * select policy — its own space, not revoked, and a write key (spec
58
- * 2026-09-29 content API contract §8): no key but a write key reads
59
- * content; the publishable key serves sign-in and presence. Split write
60
- * policies beside it. `refs` and `members` keep their `for all` write
61
- * policy, which is also their only select. */
62
- export function contentPoliciesSQL() {
63
- const statements = [];
64
- for (const table of [...CONTENT_TABLES, ...DELIVERY_TABLES, "members"]) {
65
- if (table === "refs" || table === "members") {
66
- statements.push(`drop policy if exists "smoodly_write" on "${table}";\n` +
67
- `create policy "smoodly_write" on "${table}" for all\n` +
68
- ` using (${WRITE})\n` +
69
- ` with check (${WRITE});`);
70
- continue;
71
- }
72
- statements.push(writePoliciesSQL(`"${table}"`, WRITE, "smoodly"));
73
- statements.push(`drop policy if exists "smoodly_read" on "${table}";\n` +
74
- `create policy "smoodly_read" on "${table}" for select\n` +
75
- ` using (${WRITE});`);
76
- }
77
- return statements.join("\n\n");
78
- }
79
- export function spaceLayerSQL() {
80
- const statements = [];
81
- statements.push(`
82
- -- ── Cloud state (spec 2026-09-04 §1, names 2026-09-25): three ordinary
83
- -- tables, shipped for everyone with ONE definition. Self-host fills them
84
- -- by hand or leaves them empty; smoodly.io writes rows with the service
85
- -- role. There is never cloud-only SQL on a content database.
86
- -- members: who may open /admin for a space (spec 2026-09-25 §1–§2). The
87
- -- email is stored so the Members page needs no Auth lookup — the auth
88
- -- project is not the content project in the cloud.
89
- create table if not exists "members" (
90
- ${SPACE_COL},
91
- "user_id" uuid not null,
92
- "email" text,
93
- "role" text not null default 'editor',
94
- primary key ("space_id", "user_id")
95
- );
96
-
97
- create table if not exists "space_keys" (
98
- "id" uuid primary key,
99
- "space_id" uuid not null,
100
- "kind" text not null check ("kind" in ('read', 'write')),
101
- "revoked_at" timestamptz
102
- );
103
-
104
- -- spaces: the control plane's row for a space (spec 2026-09-25 §3). No
105
- -- row = self-host: no cap, members managed in /admin. console_url set =
106
- -- smoodly.io manages the members and /admin links there.
107
- create table if not exists "spaces" (
108
- "space_id" uuid primary key,
109
- "row_cap" integer,
110
- "console_url" text
111
- );
112
-
113
- alter table "members" enable row level security;
114
- alter table "space_keys" enable row level security;
115
- alter table "spaces" enable row level security;
116
-
117
- -- ── Claim readers. The two lookups run as definer so they can read the
118
- -- service-role-only tables from inside a policy; search_path is pinned.
119
- create or replace function "smoodly_key_kind"() returns text
120
- language sql stable set search_path = '' as $$
121
- select auth.jwt() ->> 'kind';
122
- $$;
123
-
124
- create or replace function "smoodly_space_key_revoked"() returns boolean
125
- language sql stable security definer set search_path = public as $$
126
- select coalesce(
127
- (select k."revoked_at" is not null from "space_keys" k
128
- where k."id" = (auth.jwt() ->> 'key_id')::uuid),
129
- false);
130
- $$;
131
-
132
- create or replace function "smoodly_space_row_cap"() returns integer
133
- language sql stable security definer set search_path = public as $$
134
- select c."row_cap" from "spaces" c where c."space_id" = "smoodly_current_space"();
135
- $$;
136
-
137
- -- The presence gate (spec 2026-09-26 security pass §1): the caller is a
138
- -- member of the space the Realtime topic names. auth.uid() is the session
139
- -- token's subject — the anon key and a read key carry none, so a visitor
140
- -- is refused at join. Definer, because members has no read policy for
141
- -- anyone but a write key. No space claim is read: the same rule serves
142
- -- the cloud once the content project accepts smoodly.io's tokens (OQ 32).
143
- create or replace function "smoodly_presence_member"() returns boolean
144
- language sql stable security definer set search_path = public as $$
145
- select exists (select 1 from "members" m
146
- where m."user_id" = auth.uid()
147
- and realtime.topic() = 'space:' || m."space_id"::text);
148
- $$;
149
-
150
- -- ── Triggers. A key's claim wins over whatever the insert said, so an
151
- -- adapter never has to know its space and cannot write into another.
152
- -- No claim (service role, anon) leaves the given or default value —
153
- -- that is how smoodly.io writes cloud-state rows for a chosen space.
154
- create or replace function "smoodly_set_space"() returns trigger
155
- language plpgsql set search_path = '' as $$
156
- begin
157
- if auth.jwt() ->> 'space_id' is not null then
158
- new."space_id" := (auth.jwt() ->> 'space_id')::uuid;
159
- end if;
160
- return new;
161
- end;
162
- $$;
163
-
164
- -- Per table, per space. No spaces row, or a null cap = no cap (self-host). Trigger
165
- -- a_ (smoodly_set_space) runs first, so new.space_id already equals the
166
- -- current space for a keyed insert; smoodly_space_row_cap() reads the
167
- -- current space itself, so this never needs the row's space_id as an
168
- -- argument (which would let one space's insert probe another's cap).
169
- -- A service-role insert that names another space explicitly is a
170
- -- different case: there is no JWT claim, so smoodly_current_space()
171
- -- still resolves to the default space, and the cap read is the default
172
- -- space's (none) — the service role is never capped by another space's row.
173
- -- search_path is pinned empty, so every reference below is schema-qualified.
174
- create or replace function "smoodly_enforce_row_cap"() returns trigger
175
- language plpgsql set search_path = '' as $$
176
- declare
177
- cap integer;
178
- n bigint;
179
- begin
180
- cap := public."smoodly_space_row_cap"();
181
- if cap is null then
182
- return new;
183
- end if;
184
- -- Advisory only (free-tier abuse guard), not a hard invariant: two
185
- -- concurrent inserts can both observe n = cap - 1 and both pass.
186
- execute format('select count(*) from %I.%I where "space_id" = $1', tg_table_schema, tg_table_name)
187
- into n using new."space_id";
188
- if n >= cap then
189
- raise exception 'smoodly: row cap (%) reached for this space in %', cap, tg_table_name
190
- using errcode = 'check_violation';
191
- end if;
192
- return new;
193
- end;
194
- $$;`.trim());
195
- for (const table of NEEDS_SPACE_INDEX) {
196
- statements.push(`create index if not exists "${table}_space_idx" on "${table}" ("space_id");`);
197
- }
198
- // Trigger names sort a_ before b_: same-event triggers fire alphabetically.
199
- for (const table of [...CONTENT_TABLES, ...DELIVERY_TABLES, "members"]) {
200
- statements.push(`drop trigger if exists "smoodly_a_set_space" on "${table}";\n` +
201
- `create trigger "smoodly_a_set_space" before insert on "${table}"\n` +
202
- ` for each row execute function "smoodly_set_space"();`);
203
- }
204
- for (const table of CONTENT_TABLES) {
205
- statements.push(`drop trigger if exists "smoodly_b_row_cap" on "${table}";\n` +
206
- `create trigger "smoodly_b_row_cap" before insert on "${table}"\n` +
207
- ` for each row execute function "smoodly_enforce_row_cap"();`);
208
- }
209
- // Policies (permissive, so read OR write admits). The service role
210
- // bypasses them all — self-host adapters are untouched. Each function
211
- // call sits in its own scalar subquery so the planner hoists it into
212
- // an InitPlan, evaluated once per statement rather than once per row
213
- // — needed for smoodly_space_key_revoked, a security definer function
214
- // Postgres never inlines; applied to all three calls for the same win.
215
- const own = OWN;
216
- const write = WRITE;
217
- statements.push(contentPoliciesSQL());
218
- // A write key reads its own space's row — the console URL the Members
219
- // page links to (spec 2026-09-25 §3). Nothing else touches spaces or
220
- // space_keys but the two definer functions and the service role.
221
- statements.push(`drop policy if exists "smoodly_read" on "spaces";\n` +
222
- `create policy "smoodly_read" on "spaces" for select\n` +
223
- ` using (${write});`);
224
- // The package reads its schema version with the write key (the
225
- // service role bypasses anyway); nothing else sees the table. No
226
- // space_id: the schema is one per database (spec 2026-09-26 §1).
227
- statements.push(`drop policy if exists "smoodly_read" on "smoodly_meta";\n` +
228
- `create policy "smoodly_read" on "smoodly_meta" for select\n` +
229
- ` using ((select "smoodly_key_kind"()) = 'write' and not (select "smoodly_space_key_revoked"()));`);
230
- // The editing-presence channel (spec 2026-09-26 concurrency §5; gated
231
- // 2026-09-26 security pass §1): one private Realtime topic per space,
232
- // `space:<id>`. A caller receives on and sends to a topic only when a
233
- // members row of that space names their session's user — the browser
234
- // authenticates the socket with the editor's session token, never with
235
- // the public key alone. Identical in self-host and the cloud.
236
- const member = `(select public."smoodly_presence_member"())`;
237
- statements.push(`drop policy if exists "smoodly_presence_read" on "realtime"."messages";\n` +
238
- `create policy "smoodly_presence_read" on "realtime"."messages" for select\n` +
239
- ` using (${member});`);
240
- statements.push(`drop policy if exists "smoodly_presence_write" on "realtime"."messages";\n` +
241
- `create policy "smoodly_presence_write" on "realtime"."messages" for insert\n` +
242
- ` with check (${member});`);
243
- statements.push(storageSQL());
244
- return statements.join("\n\n");
245
- }
246
- /** The media bucket and its object policies (spec 2026-09-08 media §3,
247
- * §12). Public read: the CDN and next/image fetch without tokens. Writes:
248
- * a write key under its own space's prefix only; the service role
249
- * bypasses; signed uploads are minted server-side with that same key.
250
- * Schema-qualified calls: storage policies do not run with public on
251
- * the search path. Last in the migration, after the functions exist. */
252
- export function storageSQL() {
253
- return `
254
- insert into storage.buckets ("id", "name", "public", "file_size_limit")
255
- values ('media', 'media', true, 104857600)
256
- on conflict ("id") do update set "public" = excluded."public", "file_size_limit" = excluded."file_size_limit";
257
-
258
- drop policy if exists "smoodly_media_read" on storage.objects;
259
- create policy "smoodly_media_read" on storage.objects for select
260
- using ("bucket_id" = 'media');
261
-
262
- ${mediaPoliciesSQL()}`.trim();
263
- }
264
- /** The media bucket's write policies: insert, update, delete for a write
265
- * key under its own space's prefix; reads are the public select above. */
266
- export function mediaPoliciesSQL() {
267
- const write = `"bucket_id" = 'media'` +
268
- ` and (select public."smoodly_key_kind"()) = 'write'` +
269
- ` and not (select public."smoodly_space_key_revoked"())` +
270
- ` and split_part("name", '/', 1) = (select public."smoodly_current_space"())::text`;
271
- return writePoliciesSQL("storage.objects", write, "smoodly_media");
272
- }
273
- /** The three mutation RPCs are the stores' verbs, never a browser's (spec
274
- * 2026-09-26 security pass §2): executable by the service role and by
275
- * `authenticated` (the role a cloud write key carries), not by anon or
276
- * public. `smoodly_current_space` is NOT here — the stores read it through
277
- * rpc(). Last in the schema, after every definition exists. */
278
- export function grantsSQL() {
279
- // smoodly_list_entries and smoodly_list_published are callable by every
280
- // role on purpose: invoker rights and the read policies decide what a key
281
- // sees (spec 2026-09-29 content API contract §5), so there is nothing to
282
- // revoke; the published_routes view is security_invoker for the same reason.
283
- return [
284
- `revoke execute on function "smoodly_reorder"(text, uuid[]) from public, anon;`,
285
- `revoke execute on function "smoodly_reorder_pages"(uuid[]) from public, anon;`,
286
- `revoke execute on function "smoodly_rewrite_paths"(jsonb, jsonb) from public, anon;`,
287
- `revoke execute on function "smoodly_space_row_cap"() from public, anon;`,
288
- ].join("\n");
289
- }