smoodly 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/admin/auth.d.ts +10 -0
- package/dist/admin/auth.js +28 -0
- package/dist/admin/client-ops.d.ts +2 -0
- package/dist/admin/client-ops.js +18 -0
- package/dist/admin/editor/EditorView.d.ts +7 -0
- package/dist/admin/editor/EditorView.js +391 -0
- package/dist/admin/editor/autosave.d.ts +31 -0
- package/dist/admin/editor/autosave.js +108 -0
- package/dist/admin/editor/protocol.d.ts +37 -0
- package/dist/admin/editor/protocol.js +1 -0
- package/dist/admin/forms/FieldWidget.d.ts +14 -0
- package/dist/admin/forms/FieldWidget.js +54 -0
- package/dist/admin/forms/tabs.d.ts +11 -0
- package/dist/admin/forms/tabs.js +20 -0
- package/dist/admin/index.d.ts +10 -0
- package/dist/admin/index.js +8 -0
- package/dist/admin/next/bridge.d.ts +1 -0
- package/dist/admin/next/bridge.js +66 -0
- package/dist/admin/next/create-admin.d.ts +18 -0
- package/dist/admin/next/create-admin.js +14 -0
- package/dist/admin/next/index.d.ts +6 -0
- package/dist/admin/next/index.js +3 -0
- package/dist/admin/next/op-handler.d.ts +11 -0
- package/dist/admin/next/op-handler.js +23 -0
- package/dist/admin/ops-impl.d.ts +20 -0
- package/dist/admin/ops-impl.js +189 -0
- package/dist/admin/ops.d.ts +52 -0
- package/dist/admin/ops.js +1 -0
- package/dist/admin/page-path.d.ts +1 -0
- package/dist/admin/page-path.js +6 -0
- package/dist/admin/richtext.d.ts +14 -0
- package/dist/admin/richtext.js +17 -0
- package/dist/admin/serialize.d.ts +40 -0
- package/dist/admin/serialize.js +49 -0
- package/dist/admin/shell/AdminApp.d.ts +9 -0
- package/dist/admin/shell/AdminApp.js +122 -0
- package/dist/admin/shell/EntryForm.d.ts +8 -0
- package/dist/admin/shell/EntryForm.js +96 -0
- package/dist/admin/shell/ListView.d.ts +15 -0
- package/dist/admin/shell/ListView.js +14 -0
- package/dist/admin/shell/LoginView.d.ts +5 -0
- package/dist/admin/shell/LoginView.js +47 -0
- package/dist/admin/shell/pages-list.d.ts +22 -0
- package/dist/admin/shell/pages-list.js +31 -0
- package/dist/admin/shell/session.d.ts +19 -0
- package/dist/admin/shell/session.js +37 -0
- package/dist/admin/supabase-auth.d.ts +7 -0
- package/dist/admin/supabase-auth.js +23 -0
- package/dist/admin/tree-ops.d.ts +19 -0
- package/dist/admin/tree-ops.js +112 -0
- package/dist/admin/validate.d.ts +10 -0
- package/dist/admin/validate.js +19 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +19 -0
- package/dist/collections.d.ts +39 -0
- package/dist/collections.js +22 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +54 -0
- package/dist/entry-store.d.ts +74 -0
- package/dist/entry-store.js +132 -0
- package/dist/env.d.ts +16 -0
- package/dist/env.js +22 -0
- package/dist/fields.d.ts +67 -0
- package/dist/fields.js +41 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +29 -0
- package/dist/next/index.d.ts +3 -0
- package/dist/next/index.js +2 -0
- package/dist/next/meta.d.ts +10 -0
- package/dist/next/meta.js +37 -0
- package/dist/next/page-renderer.d.ts +36 -0
- package/dist/next/page-renderer.js +69 -0
- package/dist/page.d.ts +22 -0
- package/dist/page.js +35 -0
- package/dist/pairing.d.ts +33 -0
- package/dist/pairing.js +26 -0
- package/dist/ref-index.d.ts +19 -0
- package/dist/ref-index.js +0 -0
- package/dist/refs.d.ts +20 -0
- package/dist/refs.js +49 -0
- package/dist/render.d.ts +48 -0
- package/dist/render.js +41 -0
- package/dist/resolve.d.ts +22 -0
- package/dist/resolve.js +128 -0
- package/dist/revalidate.d.ts +8 -0
- package/dist/revalidate.js +34 -0
- package/dist/schema.d.ts +57 -0
- package/dist/schema.js +40 -0
- package/dist/section-wrapper.d.ts +11 -0
- package/dist/section-wrapper.js +19 -0
- package/dist/sql-space.d.ts +6 -0
- package/dist/sql-space.js +172 -0
- package/dist/sql.d.ts +3 -0
- package/dist/sql.js +162 -0
- package/dist/store.d.ts +73 -0
- package/dist/store.js +145 -0
- package/dist/supabase-entry-store.d.ts +26 -0
- package/dist/supabase-entry-store.js +191 -0
- package/dist/supabase-store.d.ts +26 -0
- package/dist/supabase-store.js +180 -0
- package/dist/zones.d.ts +31 -0
- package/dist/zones.js +23 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laniakea Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# smoodly (core package)
|
|
2
|
+
|
|
3
|
+
Define a component once in code — it's instantly a typed, editable,
|
|
4
|
+
AI-addressable building block your editors can compose, within the rails you
|
|
5
|
+
set. Content lives in Supabase; the admin mounts into your own Next.js app.
|
|
6
|
+
|
|
7
|
+
npx create-smoodly-app my-site
|
|
8
|
+
|
|
9
|
+
A self-hosted app needs three environment variables: `SMOODLY_URL`,
|
|
10
|
+
`SMOODLY_SECRET_KEY` and `SMOODLY_PUBLISHABLE_KEY`.
|
|
11
|
+
Source and docs: https://github.com/laniakea-studio/smoodly-cms
|
|
12
|
+
|
|
13
|
+
The registration runtime — the real implementation of the API designed in
|
|
14
|
+
Phase 0 (`sketches/phase0/` remains the readable spec; `docs/DESIGN.md`
|
|
15
|
+
holds the decisions). Test-driven; run with `npm test`.
|
|
16
|
+
|
|
17
|
+
Implemented:
|
|
18
|
+
|
|
19
|
+
- `f.*` field builders — inert, immutable, JSON-serializable descriptors
|
|
20
|
+
(ref thunks excepted); validation lives server/admin-side by design rule
|
|
21
|
+
- `smoodly.schema.section/element` — descriptor capture + the
|
|
22
|
+
fields-only-`localized()` rule enforced at construction
|
|
23
|
+
- `smoodly.section/element(schema, View)` — the typed pairing: returns a
|
|
24
|
+
real component carrying `.schema`; drift between schema and view is a
|
|
25
|
+
compile error at the pairing; style select defaults apply in hard-coded
|
|
26
|
+
JSX use; `Props<typeof schema>` infers required/optional/select types
|
|
27
|
+
- `z.sections([...]).max(n)` / `z.freeform` / `z.locked` — component
|
|
28
|
+
references in, names stored
|
|
29
|
+
- `smoodly.toolset` — plain values, element refs serialized to names
|
|
30
|
+
- `smoodly.collection` — descriptors + admin hints; ref thunks resolve
|
|
31
|
+
post-evaluation and infer the target entry shape
|
|
32
|
+
- `defineConfig` — registry name-uniqueness + the automatic standard-style
|
|
33
|
+
merge (`resolvedStyles`: section keys win, `styleDefaults: false|{omit}`)
|
|
34
|
+
- `<smoodly.Section>` — root wrapper: `as`, string styles → kebab-cased
|
|
35
|
+
data attributes, className/style passthrough
|
|
36
|
+
|
|
37
|
+
- `smoodly.schema.page` / `smoodly.page` / `<Zone>` / `renderPage` — the
|
|
38
|
+
pages runtime: policies normalized to plain data, tree nodes mapped to
|
|
39
|
+
registered components, fail-soft on unknown types
|
|
40
|
+
- `collectionSQL` / `coreTablesSQL` — the stable schema (DESIGN.md §3):
|
|
41
|
+
`coreTablesSQL` emits the once-ever core tables including `entries`
|
|
42
|
+
(`collection` + `fields`/`i18n` jsonb, `space_id` multi-tenancy with
|
|
43
|
+
a default space for self-hosted, `sort` for manual ordering, ONE
|
|
44
|
+
generic slug-uniqueness index across all spaces and collections);
|
|
45
|
+
`collectionSQL` emits only the disposable per-collection guardrails —
|
|
46
|
+
a published-only read view over `entries` (typed casts, snake_case
|
|
47
|
+
aliases, refs as plain id values) and partial btree indexes for
|
|
48
|
+
declared orderBy fields
|
|
49
|
+
- `PageStore` contract + `MemoryPageStore` — draft/publish semantics
|
|
50
|
+
(versions accumulate; publish moves a pointer; drafts never leak into
|
|
51
|
+
published reads). The contract is a reusable suite
|
|
52
|
+
(`test/page-store-contract.ts`); every adapter runs the same tests.
|
|
53
|
+
- `SupabasePageStore` — the Supabase adapter over pages/page_versions
|
|
54
|
+
(a server-side client from `createSmoodlyClient`: the service role in
|
|
55
|
+
self-host, a write key in the cloud). Contract-tested against a
|
|
56
|
+
real local stack: `supabase start` at the repo root (the generated
|
|
57
|
+
DDL is its one migration), then `npm run test:supabase`; plain
|
|
58
|
+
`npm test` skips it and never needs Docker.
|
|
59
|
+
- `resolveTree` — the batched ref-resolution pass: one fetch per
|
|
60
|
+
collection per round, ref-site depth modes (bare ref = entry only,
|
|
61
|
+
`.resolve(names)` = named refs one level, bare `.resolve()` = whole
|
|
62
|
+
chain), per-path cycle cutting with shallow copies, optional `maxDepth`
|
|
63
|
+
safety valve (default unlimited)
|
|
64
|
+
|
|
65
|
+
- `collectEntryRefs` / `collectTreeRefs` — the refs-index feeders: walk
|
|
66
|
+
an entry or a page tree against its schemas and list every outgoing
|
|
67
|
+
reference edge (derived, rebuildable, never the source of truth)
|
|
68
|
+
- Page saves feed the refs index — a page's rows are the UNION of its
|
|
69
|
+
draft and published trees' edges (source kind `page`), rewritten on
|
|
70
|
+
every `saveDraft`/`publish`, so cross-kind safe-delete holds while
|
|
71
|
+
either tree shows an entry. Publish integrity: `publish` walks the
|
|
72
|
+
draft tree directly and blocks on missing/unpublished entries.
|
|
73
|
+
Revalidation fan-out: `affectedPageSlugs` — reverse BFS over refs
|
|
74
|
+
(entry ← entry ← page, cycle-safe, optional `maxDepth`) returning
|
|
75
|
+
published slugs for the Next glue to `revalidateTag`. All wired via
|
|
76
|
+
store constructor options (`sections`, `entries`, shared
|
|
77
|
+
`MemoryRefIndex` in memory); the cross-store contract
|
|
78
|
+
(`test/page-refs-contract.ts`) runs against memory AND Supabase.
|
|
79
|
+
- `EntryStore` contract + `MemoryEntryStore` + `SupabaseEntryStore` —
|
|
80
|
+
collection-entry persistence (status-only versioning in v1): saves
|
|
81
|
+
rewrite the entry's refs rows, `referencesTo` answers the reverse
|
|
82
|
+
question, `getMany` is the batched read whose signature is
|
|
83
|
+
`resolveTree`'s `EntryFetcher` (with `{ status: "published" }` for the
|
|
84
|
+
published-route read — a post-publish demotion resolves as missing
|
|
85
|
+
instead of leaking draft fields — DESIGN.md §3 "Page edges in refs"), deletes are blocked while
|
|
86
|
+
referenced,
|
|
87
|
+
and `reorder` rewrites sort 1..N through the `smoodly_reorder` SQL
|
|
88
|
+
function (one statement, `updated_at` untouched). Same
|
|
89
|
+
reusable-contract pattern as PageStore; `npm run test:supabase` runs
|
|
90
|
+
both adapters against the local stack
|
|
91
|
+
|
|
92
|
+
- Proven end-to-end in `examples/site` — a real Next.js app wiring the
|
|
93
|
+
config + Supabase stores into routes: tag-cached resolved trees,
|
|
94
|
+
draft-mode preview, server-action writes with revalidation fan-out
|
|
95
|
+
|
|
96
|
+
- The space layer (spec 2026-09-04 §3): `space_id` on every content
|
|
97
|
+
table, `smoodly_current_space()`, the insert trigger, the row cap, the
|
|
98
|
+
three cloud-state tables (`editors`, `space_keys`, `space_caps`) and
|
|
99
|
+
the policy set. Service role bypasses it; a write key is scoped to its
|
|
100
|
+
space. Contract suites run in both modes; `test/isolation.supabase.test.ts`
|
|
101
|
+
proves isolation. Generated per-collection views are `security_invoker`,
|
|
102
|
+
so they read through the same policy set rather than their owner's
|
|
103
|
+
privileges. Live acceptance test in `test/rls.supabase.test.ts`. Note
|
|
104
|
+
for self-host: the anon key now reads published rows straight off the
|
|
105
|
+
base tables (`pages`, `entries` including the `i18n` overlay, a page's
|
|
106
|
+
published `page_versions` tree, `global_sections`), not only through
|
|
107
|
+
the generated views.
|
|
108
|
+
|
|
109
|
+
- Admin login: `supabaseAdminAuth` + `gateAdminOp` around the ops seam;
|
|
110
|
+
`LoginView` + `useAdminSession` in the shell; `createSmoodlyAdmin({ auth })`.
|
|
111
|
+
Env: `smoodlyEnv()` / `createSmoodlyClient()`.
|
|
112
|
+
|
|
113
|
+
- The admin, real and mounted: `smoodly/admin` (the AdminOps seam —
|
|
114
|
+
`createAdminOps`/`runAdminOp` server-side over the stores,
|
|
115
|
+
`makeClientOps` client-side forwarding `{ path, args }` through one
|
|
116
|
+
server action; typed `OpResult`, never throws — `serializeAdminConfig`
|
|
117
|
+
turns the registry into the admin's nav/forms; `validateFields` +
|
|
118
|
+
tree-ops for pure canvas mutations), `smoodly/admin/next`
|
|
119
|
+
(`createSmoodlyAdmin` binds registry + op action into `AdminPage`/
|
|
120
|
+
`AdminLayout` for the app's `[[...segments]]` route group;
|
|
121
|
+
`createAdminOpHandler` is the one server action the client ops call),
|
|
122
|
+
and `smoodly/bridge` (`EditorBridge`, the only piece that ships to the
|
|
123
|
+
SITE side — runs inside the draft-route iframe, reports node/zone
|
|
124
|
+
geometry, forwards select/hover, executes refresh/scroll). Shell
|
|
125
|
+
(registry-derived nav, list views, segment-based internal routing),
|
|
126
|
+
entry form (widgets by field type, validation errors surfaced inline,
|
|
127
|
+
save/publish/unpublish/delete with the safe-delete guard's message
|
|
128
|
+
shown verbatim), and the editor view (dark instrument around a bright
|
|
129
|
+
canvas — the iframe is the site's own draft route, pixel-identical by
|
|
130
|
+
construction; every gesture mutates client tree state through pure
|
|
131
|
+
ops, a debounced `saveDraft` persists, and the bridge refreshes the
|
|
132
|
+
iframe on the save ack — refresh-on-change preview, not a live DOM
|
|
133
|
+
sync) are all real, in-house. The autosave itself is a framework-free
|
|
134
|
+
controller (`createDraftAutosave`: debounce, doubling capped retry,
|
|
135
|
+
pre-publish `flush`) wrapped by `useDraftAutosave`; the controller is
|
|
136
|
+
unit-tested with fake timers, the hook only for its initial snapshot. Proven end-to-end in
|
|
137
|
+
`examples/site/app/(smoodly)/admin` against the local Supabase stack —
|
|
138
|
+
see its README's acceptance walk.
|
|
139
|
+
|
|
140
|
+
- `smoodly/next` — `createPageRenderer`, the site-route seam: draft
|
|
141
|
+
branch, tag-cached published read, ref resolution, 404 and editor
|
|
142
|
+
bridge behind one `renderSmoodlyPage(Page, { slug?, searchParams? })`,
|
|
143
|
+
plus `smoodlyMetadata` for `generateMetadata`. Both await Next's
|
|
144
|
+
`connection()` before reading the stores, so a fixed-slug route is
|
|
145
|
+
request-rendered without a `force-dynamic` export and `next build`
|
|
146
|
+
never reaches the database (`test/next-page-renderer.test.tsx`). Bound
|
|
147
|
+
once by the app, like `createAdminOpHandler`. Page schemas may declare a fixed `slug`
|
|
148
|
+
(a singleton owned by one route file); the admin reads it to filter
|
|
149
|
+
New Page and protect the record. `defineConfig({ homePageSlug })`
|
|
150
|
+
(default `"home"`) names the slug the site root serves; the admin's
|
|
151
|
+
`pagePath()` maps it to `/` — canvas URL and Pages list — and every
|
|
152
|
+
other slug to `/<slug>`.
|
|
153
|
+
|
|
154
|
+
- Locked zones are usable: `fillLockedZones` (a pure tree op) materializes
|
|
155
|
+
the one node of every empty locked zone from the component's `sample`
|
|
156
|
+
when the editor loads a draft, and the change rides the normal debounced
|
|
157
|
+
save. No gesture can insert into or remove from a locked zone, so this is
|
|
158
|
+
the only way the node gets there — it also covers pages that existed
|
|
159
|
+
before a locked zone was added to their schema.
|
|
160
|
+
|
|
161
|
+
Not yet: richtext components (TipTap; `plainToRichtext`/
|
|
162
|
+
`richtextToPlain` exist but no rich editor widget), drag-and-drop
|
|
163
|
+
reordering (move-up/move-down buttons only), undo/redo, sample/
|
|
164
|
+
placeholder content marking, editor localization (the
|
|
165
|
+
locale switcher — examples/site is single-locale so it stays hidden).
|
|
166
|
+
|
|
167
|
+
## Releasing
|
|
168
|
+
|
|
169
|
+
Both packages move together. From the repo root:
|
|
170
|
+
|
|
171
|
+
npm version 0.0.2 -w smoodly -w create-smoodly-app --no-git-tag-version
|
|
172
|
+
npm run build
|
|
173
|
+
npm publish -w smoodly --access public
|
|
174
|
+
npm publish -w create-smoodly-app --access public
|
|
175
|
+
git commit -am "release: 0.0.2" && git tag v0.0.2 && git push --tags origin main
|
|
176
|
+
|
|
177
|
+
`prepublishOnly` rebuilds each package; the CLI's template is regenerated from
|
|
178
|
+
`examples/site` in that build, so the template always matches the example
|
|
179
|
+
site at the tagged commit.
|
|
180
|
+
|
|
181
|
+
## Follow-ups (logged 2026-09-04)
|
|
182
|
+
|
|
183
|
+
- Cloud-ready foundation — done, including the self-host init
|
|
184
|
+
(`create-smoodly-app`, spec 2026-09-04). Remaining from the spec: none
|
|
185
|
+
in the package. The two-mode contract suites and the isolation suite
|
|
186
|
+
run in CI in the `smoodly-supabase` job (a local stack on the runner) — if that job
|
|
187
|
+
proves flaky, the fallback is a Postgres service container applying
|
|
188
|
+
the one migration.
|
|
189
|
+
- Once the policy-based published read exists, decide whether it becomes the canonical headless read path and the generated per-collection views turn optional (cloud has no views).
|
|
190
|
+
- Add an index on `pages(published_version_id)`: the `page_versions` read policy's `exists` probe runs on every anon/read-key read and has no supporting index.
|
|
191
|
+
- A malformed `space_id`/`key_id` claim raises `22P02` inside the policy: fails closed but surfaces as a 500. Only reachable from a validly-signed key, so it is a cloud key-minting guard, not a request-path one.
|
|
192
|
+
- Authorization trusts the JWT claims alone; `smoodly_space_key_revoked()` checks only `revoked_at`. When cloud key-minting exists, make it a full key check (the claims must match the `space_keys` row).
|
|
193
|
+
- `isEditor` reads `editors` with no space filter: under a cloud write key RLS scopes it, but a service-role client bypasses RLS and would admit an editor of any space. Harmless in self-host (one space); never use a service-role client against the shared content project.
|
|
194
|
+
- The `sb_secret_` branch of `clientOptions` is unit-tested for shape only; the local stack uses legacy JWT keys, so a new-format secret key has not been exercised live against PostgREST and the GoTrue admin API.
|
|
195
|
+
- The migration is not re-appliable to a pre-rename (`project_id`) database: `create table if not exists` leaves it untouched and the `space_id` policies then fail. No installs predate the rename; fold into DESIGN.md OQ #2 (schema upgrades).
|
|
196
|
+
- `examples/site/lib/smoodly/index.ts` should carry `import "server-only"` now that it also exports `publicAuth` — a client-side import would fail loudly at build instead of at runtime.
|
|
197
|
+
- `exports` has no `"./package.json"` entry, so tooling that reads a dependency's `package.json` through the exports map cannot. One line to add when something needs it.
|
|
198
|
+
- `src/ref-index.ts` contains two raw NUL bytes used as key separators, which makes git show the file as binary in diffs. Replacing them with `\0` escapes would make it diffable again.
|
|
199
|
+
|
|
200
|
+
## Follow-ups (logged 2026-09-03)
|
|
201
|
+
|
|
202
|
+
- Empty locked zones on the site side: `renderPage` renders nothing for a locked zone with no node. A page that predates the zone shows no prefooter on the published site until an editor opens it (the editor fills and saves). Options: render the component from `sample` when the zone is empty, or fill on `createPage`/schema load server-side. Fine for now — seeded and admin-created pages both arrive filled.
|
|
203
|
+
|
|
204
|
+
- Editor canvas URL mapping: now `homePageSlug` config (done 2026-09-03); remaining follow-up is a registration-declared `path` for fixed pages at arbitrary paths.
|
|
205
|
+
- `serialize.ts` structural cast duplicates `PageSchema`; tighten `Registered.schema` so the cast goes.
|
|
206
|
+
- Pages list: no re-entrancy guard on a fast double-click of "New page" / a not-created row, and the row `onClick` promise is unhandled (a transport throw is an unhandled rejection); fix both together with a `.catch` → alert.
|
|
207
|
+
- `pagesRows`: pin with a test the case of a record at a fixed slug whose `template` is an open registration (self-healing today: `/` 404s, `/<slug>` serves it, delete is allowed, then the singleton row reappears).
|
|
208
|
+
- Catch-all `templates` map should filter to open registrations to state its intent (unreachable today).
|
|
209
|
+
- The no-database build is the real proof that routes never prerender against the stores; the unit test only stubs `next/server`. Script it (`SUPABASE_URL=http://127.0.0.1:9 npx next build` in `examples/site`, expect `ƒ /`) and add it to CI, which today runs only the package's tests and typecheck — so a regression is caught when Next changes `connection()` semantics.
|
|
210
|
+
- Next acceptance walk: start from a database with no `home` record so the "not created" singleton row is exercised.
|
|
211
|
+
- `EditorView` split, remaining seams (autosave extracted 2026-09-03): pull the canvas overlay and the insert picker into components when the dnd-kit outline work starts; the sidebar form stays put until inline editing lands (DESIGN.md §3 puts TipTap in the canvas, not the sidebar).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AdminOpCall, OpResult } from "./ops.ts";
|
|
2
|
+
export type AdminAuth = {
|
|
3
|
+
/** The user behind a session token, or null when the auth project rejects it. */
|
|
4
|
+
verify(token: string): Promise<{
|
|
5
|
+
userId: string;
|
|
6
|
+
} | null>;
|
|
7
|
+
/** Does `editors` hold this user for the current space? */
|
|
8
|
+
isEditor(userId: string): Promise<boolean>;
|
|
9
|
+
};
|
|
10
|
+
export declare function gateAdminOp(auth: AdminAuth, call: AdminOpCall, run: (call: AdminOpCall) => Promise<OpResult<unknown>>): Promise<OpResult<unknown>>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const unauthorized = (message) => ({ ok: false, code: "unauthorized", message });
|
|
2
|
+
export async function gateAdminOp(auth, call, run) {
|
|
3
|
+
const token = typeof call === "object" && call !== null && typeof call.token === "string"
|
|
4
|
+
? call.token
|
|
5
|
+
: null;
|
|
6
|
+
if (!token)
|
|
7
|
+
return unauthorized("smoodly: sign in to use the admin.");
|
|
8
|
+
let session;
|
|
9
|
+
let editor;
|
|
10
|
+
try {
|
|
11
|
+
session = await auth.verify(token);
|
|
12
|
+
if (!session)
|
|
13
|
+
return unauthorized("smoodly: your session has expired — sign in again.");
|
|
14
|
+
editor = await auth.isEditor(session.userId);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.error("smoodly: auth check failed", e);
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
code: "internal",
|
|
21
|
+
message: "smoodly: could not check your session.",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (!editor) {
|
|
25
|
+
return unauthorized("smoodly: this account is not an editor of this site.");
|
|
26
|
+
}
|
|
27
|
+
return run(call);
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const GROUPS = {
|
|
2
|
+
pages: ["list", "get", "create", "saveDraft", "publish", "delete"],
|
|
3
|
+
entries: ["list", "get", "create", "save", "setStatus", "delete"],
|
|
4
|
+
preview: ["enable", "disable"],
|
|
5
|
+
};
|
|
6
|
+
export function makeClientOps(action, token) {
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const [group, methods] of Object.entries(GROUPS)) {
|
|
9
|
+
out[group] = {};
|
|
10
|
+
for (const method of methods) {
|
|
11
|
+
out[group][method] = (...args) => {
|
|
12
|
+
const t = token?.() ?? null;
|
|
13
|
+
return action({ path: `${group}.${method}`, args, ...(t ? { token: t } : {}) });
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|