smoodly 0.0.39 → 0.0.41
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/dist/admin/next/create-admin.js +4 -1
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/cli/space-io.d.ts +5 -4
- package/dist/cli/space-io.js +12 -5
- package/dist/client.js +29 -1
- package/dist/content/content-api.d.ts +6 -0
- package/dist/content/content-api.js +6 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +23 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next/page-renderer.js +17 -7
- package/dist/next/preview-route.js +3 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -28,6 +28,9 @@ export function createSmoodlyAdmin(input) {
|
|
|
28
28
|
// Presence is the one thing that needs it: a failed lookup (an unmigrated project, an
|
|
29
29
|
// unreachable database) is logged and turns presence off, so the shell still reaches
|
|
30
30
|
// sign-in, the schema notice or the admin-path ErrorPane.
|
|
31
|
+
// Auth first: an admin that cannot start (a missing publishable key) fails on its own
|
|
32
|
+
// error, not after a presence line that suggests it runs without presence.
|
|
33
|
+
const auth = input.auth();
|
|
31
34
|
let presenceConfig;
|
|
32
35
|
try {
|
|
33
36
|
presenceConfig = await input.presence?.();
|
|
@@ -35,7 +38,7 @@ export function createSmoodlyAdmin(input) {
|
|
|
35
38
|
catch (e) {
|
|
36
39
|
console.error("smoodly: editing presence is off —", e instanceof Error ? e.message : e);
|
|
37
40
|
}
|
|
38
|
-
return (_jsxs("div", { className: "sm-admin", suppressHydrationWarning: true, children: [_jsx(AdminStyles, {}), _jsx(AdminApp, { registry: registry, op: input.op, auth:
|
|
41
|
+
return (_jsxs("div", { className: "sm-admin", suppressHydrationWarning: true, children: [_jsx(AdminStyles, {}), _jsx(AdminApp, { registry: registry, op: input.op, auth: auth, presenceConfig: presenceConfig }), children] }));
|
|
39
42
|
}
|
|
40
43
|
return { AdminPage, AdminLayout };
|
|
41
44
|
}
|
|
@@ -11,7 +11,7 @@ export function supabaseAdminAuth(input) {
|
|
|
11
11
|
let client = null;
|
|
12
12
|
const auth = () => {
|
|
13
13
|
if (!input.authKey) {
|
|
14
|
-
throw new Error("smoodly: set SMOODLY_PUBLISHABLE_KEY in .env.local — the admin signs in with it (Deploying: https://smoodly.io/docs/getting-started/deploying).");
|
|
14
|
+
throw new Error("smoodly: set SMOODLY_PUBLISHABLE_KEY in .env.local, or in your host's environment variables — the admin signs in with it (Deploying: https://smoodly.io/docs/getting-started/deploying).");
|
|
15
15
|
}
|
|
16
16
|
return (client ?? (client = createClient(input.authUrl, input.authKey, { auth: { persistSession: false, autoRefreshToken: false } })));
|
|
17
17
|
};
|
package/dist/cli/space-io.d.ts
CHANGED
|
@@ -7,10 +7,11 @@ export type SpaceConnection = {
|
|
|
7
7
|
export declare const MEDIA_BUCKET = "media";
|
|
8
8
|
/** Rows per request. A host may cap below it, so a read stops at the first empty page, not the first short one. */
|
|
9
9
|
export declare const PAGE_ROWS = 1000;
|
|
10
|
-
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). */
|
|
11
|
-
export declare function loadAppEnv(cwd: string, env?: Record<string, string | undefined>):
|
|
12
|
-
/** The app's content client, from its env.
|
|
13
|
-
|
|
10
|
+
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). Returns the files it read. */
|
|
11
|
+
export declare function loadAppEnv(cwd: string, env?: Record<string, string | undefined>): string[];
|
|
12
|
+
/** The app's content client, from its env. Run outside the app, the missing keys are a missing
|
|
13
|
+
* folder, so the error names the folder it looked in. */
|
|
14
|
+
export declare function connectFromEnv(cwd: string, env?: Record<string, string | undefined>): SpaceConnection;
|
|
14
15
|
export { readSchemaVersion } from "../schema-read.ts";
|
|
15
16
|
export declare function schemaRefusal(missing: "key" | "schema", command: "Export" | "Import"): string;
|
|
16
17
|
/** Every row of one table in one space, paged in primary-key order. The space filter is explicit: a service-role key sees every space. */
|
package/dist/cli/space-io.js
CHANGED
|
@@ -13,22 +13,29 @@ import { ROW_KEY } from "../space-transfer.js";
|
|
|
13
13
|
export const MEDIA_BUCKET = "media";
|
|
14
14
|
/** Rows per request. A host may cap below it, so a read stops at the first empty page, not the first short one. */
|
|
15
15
|
export const PAGE_ROWS = 1000;
|
|
16
|
-
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). */
|
|
16
|
+
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). Returns the files it read. */
|
|
17
17
|
export function loadAppEnv(cwd, env = process.env) {
|
|
18
|
+
const read = [];
|
|
18
19
|
for (const name of [".env.local", ".env"]) {
|
|
19
20
|
const file = join(cwd, name);
|
|
20
21
|
if (!existsSync(file))
|
|
21
22
|
continue;
|
|
23
|
+
read.push(name);
|
|
22
24
|
for (const [key, value] of Object.entries(parseEnv(readFileSync(file, "utf8")))) {
|
|
23
25
|
if (env[key] === undefined)
|
|
24
26
|
env[key] = value;
|
|
25
27
|
}
|
|
26
28
|
}
|
|
29
|
+
return read;
|
|
27
30
|
}
|
|
28
|
-
/** The app's content client, from its env.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
/** The app's content client, from its env. Run outside the app, the missing keys are a missing
|
|
32
|
+
* folder, so the error names the folder it looked in. */
|
|
33
|
+
export function connectFromEnv(cwd, env = process.env) {
|
|
34
|
+
const read = loadAppEnv(cwd, env);
|
|
35
|
+
if (read.length === 0 && (!env.SMOODLY_URL || !env.SMOODLY_SECRET_KEY)) {
|
|
36
|
+
throw new Error(`smoodly: no .env.local or .env in ${cwd} — run this in your app's folder.`);
|
|
37
|
+
}
|
|
38
|
+
return { db: createSmoodlyClient(resolveSmoodlyEnv(env)) };
|
|
32
39
|
}
|
|
33
40
|
export { readSchemaVersion } from "../schema-read.js";
|
|
34
41
|
export function schemaRefusal(missing, command) {
|
package/dist/client.js
CHANGED
|
@@ -11,11 +11,39 @@ export function clientOptions(env) {
|
|
|
11
11
|
// without one (spec 2026-09-30 tester findings §4): a self-host service role is a valid apikey.
|
|
12
12
|
return { url: env.url, key: env.publishableKey || env.secretKey, headers: { Authorization: `Bearer ${env.secretKey}` } };
|
|
13
13
|
}
|
|
14
|
+
/** fetch that names the variable behind two failures supabase-js reports bare: the network
|
|
15
|
+
* ("TypeError: fetch failed") and PostgREST refusing the key. */
|
|
16
|
+
function namedFetch(url) {
|
|
17
|
+
return async (input, init) => {
|
|
18
|
+
let res;
|
|
19
|
+
try {
|
|
20
|
+
res = await fetch(input, init);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (e instanceof Error && e.name === "AbortError")
|
|
24
|
+
throw e;
|
|
25
|
+
throw new TypeError(`can't reach SMOODLY_URL (${url}): ${e instanceof Error ? e.message : String(e)}`);
|
|
26
|
+
}
|
|
27
|
+
if (res.status !== 401)
|
|
28
|
+
return res;
|
|
29
|
+
// PostgREST's key errors are PGRST300–303; their message varies by version ("JWT cryptographic
|
|
30
|
+
// operation failed", "No suitable key or wrong key type").
|
|
31
|
+
const body = (await res.clone().json().catch(() => null));
|
|
32
|
+
if (typeof body?.message !== "string")
|
|
33
|
+
return res;
|
|
34
|
+
if (!(typeof body.code === "string" && /^PGRST30\d$/.test(body.code)) && !/JWT/i.test(body.message))
|
|
35
|
+
return res;
|
|
36
|
+
const headers = new Headers(res.headers);
|
|
37
|
+
headers.delete("content-length");
|
|
38
|
+
const message = `SMOODLY_SECRET_KEY is not a valid key for SMOODLY_URL (${url}): ${body.message}`;
|
|
39
|
+
return new Response(JSON.stringify({ ...body, message }), { status: 401, statusText: res.statusText, headers });
|
|
40
|
+
};
|
|
41
|
+
}
|
|
14
42
|
/** The server-side client the stores are built on. */
|
|
15
43
|
export function createSmoodlyClient(env) {
|
|
16
44
|
const options = clientOptions(env);
|
|
17
45
|
return createClient(options.url, options.key, {
|
|
18
|
-
global: { headers: options.headers },
|
|
46
|
+
global: { headers: options.headers, fetch: namedFetch(options.url) },
|
|
19
47
|
auth: { persistSession: false, autoRefreshToken: false },
|
|
20
48
|
});
|
|
21
49
|
}
|
|
@@ -43,6 +43,12 @@ export declare function createSmoodlyContent(deps: ContentDeps): {
|
|
|
43
43
|
title?: string;
|
|
44
44
|
}): Promise<PageRecord>;
|
|
45
45
|
publish: (id: string, locale: string) => Promise<PageRecord>;
|
|
46
|
+
/** Every page present in `locale` (the default language unless given), code-owned ones included and
|
|
47
|
+
* folders left out, optionally only one page type: what a script walks to change many pages. */
|
|
48
|
+
list(options?: {
|
|
49
|
+
locale?: string;
|
|
50
|
+
template?: string;
|
|
51
|
+
}): Promise<PageRecord[]>;
|
|
46
52
|
/** A code-owned address with no record gets it created the way the admin's Pages list does (spec §2). */
|
|
47
53
|
find(locale: string, path: string): Promise<PageRecord | null>;
|
|
48
54
|
};
|
|
@@ -107,6 +107,12 @@ export function createSmoodlyContent(deps) {
|
|
|
107
107
|
}));
|
|
108
108
|
},
|
|
109
109
|
publish: (id, locale) => must(ops.pages.publish(id, locale)),
|
|
110
|
+
/** Every page present in `locale` (the default language unless given), code-owned ones included and
|
|
111
|
+
* folders left out, optionally only one page type: what a script walks to change many pages. */
|
|
112
|
+
async list(options = {}) {
|
|
113
|
+
const locale = options.locale ?? def;
|
|
114
|
+
return (await must(ops.pages.list())).filter((p) => p.template !== null && p.locales[locale] !== undefined && (options.template === undefined || p.template === options.template));
|
|
115
|
+
},
|
|
110
116
|
/** A code-owned address with no record gets it created the way the admin's Pages list does (spec §2). */
|
|
111
117
|
async find(locale, path) {
|
|
112
118
|
const found = await findPage(locale, path);
|
package/dist/env.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export type SmoodlyEnv = {
|
|
|
15
15
|
* publishable key, so their callers pass "admin" (spec 2026-09-30 tester findings §4). A key
|
|
16
16
|
* in the wrong variable or of no known form is named here, not by a failed read later. */
|
|
17
17
|
export declare function resolveSmoodlyEnv(env: Record<string, string | undefined>, need?: "site" | "admin"): SmoodlyEnv;
|
|
18
|
+
/** The site's own address for the root layout's `metadataBase`, so canonical, hreflang and social
|
|
19
|
+
* image links are absolute. A value without a scheme is named here, not by a bare "Invalid URL" on
|
|
20
|
+
* every page; an unset one falls back to localhost and warns once in production, where it would
|
|
21
|
+
* put localhost into every page's head. */
|
|
22
|
+
export declare function siteUrl(env?: Record<string, string | undefined>): URL;
|
|
18
23
|
/** `resolveSmoodlyEnv(process.env)` — call lazily, never at module load,
|
|
19
24
|
* so `next build` without env still succeeds. */
|
|
20
25
|
export declare const smoodlyEnv: (need?: "site" | "admin") => SmoodlyEnv;
|
package/dist/env.js
CHANGED
|
@@ -25,7 +25,7 @@ function jwtRole(v) {
|
|
|
25
25
|
export function resolveSmoodlyEnv(env, need = "site") {
|
|
26
26
|
const missing = (need === "admin" ? ADMIN_NEEDS : SITE_NEEDS).filter((name) => !env[name]);
|
|
27
27
|
if (missing.length > 0)
|
|
28
|
-
throw new Error(`smoodly: set ${missing.join(", ")} in .env.local (Deploying: ${DEPLOYING}).`);
|
|
28
|
+
throw new Error(`smoodly: set ${missing.join(", ")} in .env.local, or in your host's environment variables (Deploying: ${DEPLOYING}).`);
|
|
29
29
|
const secretKey = env.SMOODLY_SECRET_KEY;
|
|
30
30
|
const publishableKey = env.SMOODLY_PUBLISHABLE_KEY ?? "";
|
|
31
31
|
if (secretKey.startsWith("sb_publishable_") || jwtRole(secretKey) === "anon") {
|
|
@@ -41,8 +41,30 @@ export function resolveSmoodlyEnv(env, need = "site") {
|
|
|
41
41
|
throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY is neither an sb_publishable_ key nor a JWT — copy the publishable (anon) key from your Supabase project.");
|
|
42
42
|
}
|
|
43
43
|
const url = env.SMOODLY_URL;
|
|
44
|
+
if (!/^https?:\/\/[^/\s]+/.test(url)) {
|
|
45
|
+
throw new Error(`smoodly: SMOODLY_URL "${url}" is not an http(s) address — copy the project URL from your Supabase project.`);
|
|
46
|
+
}
|
|
44
47
|
return { url, secretKey, publishableKey, authUrl: env.SMOODLY_AUTH_URL || url, authKey: env.SMOODLY_AUTH_KEY || publishableKey };
|
|
45
48
|
}
|
|
49
|
+
let siteUrlWarned = false;
|
|
50
|
+
/** The site's own address for the root layout's `metadataBase`, so canonical, hreflang and social
|
|
51
|
+
* image links are absolute. A value without a scheme is named here, not by a bare "Invalid URL" on
|
|
52
|
+
* every page; an unset one falls back to localhost and warns once in production, where it would
|
|
53
|
+
* put localhost into every page's head. */
|
|
54
|
+
export function siteUrl(env = process.env) {
|
|
55
|
+
const value = env.SITE_URL;
|
|
56
|
+
if (!value) {
|
|
57
|
+
if (env.NODE_ENV === "production" && !siteUrlWarned) {
|
|
58
|
+
siteUrlWarned = true;
|
|
59
|
+
console.warn(`smoodly: SITE_URL is not set — the page head's links point at http://localhost:3000 (Deploying: ${DEPLOYING}).`);
|
|
60
|
+
}
|
|
61
|
+
return new URL("http://localhost:3000");
|
|
62
|
+
}
|
|
63
|
+
if (!/^https?:\/\/[^/\s]+/.test(value)) {
|
|
64
|
+
throw new Error(`smoodly: SITE_URL "${value}" is not an http(s) address — set it like https://www.example.com.`);
|
|
65
|
+
}
|
|
66
|
+
return new URL(value);
|
|
67
|
+
}
|
|
46
68
|
/** `resolveSmoodlyEnv(process.env)` — call lazily, never at module load,
|
|
47
69
|
* so `next build` without env still succeeds. */
|
|
48
70
|
export const smoodlyEnv = (need = "site") => resolveSmoodlyEnv(process.env, need);
|
package/dist/index.d.ts
CHANGED
|
@@ -129,7 +129,7 @@ export declare const smoodly: {
|
|
|
129
129
|
};
|
|
130
130
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
|
|
131
131
|
export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
|
|
132
|
-
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig } from "./env.ts";
|
|
132
|
+
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig, siteUrl } from "./env.ts";
|
|
133
133
|
export type { SmoodlyEnv, PresenceConfig } from "./env.ts";
|
|
134
134
|
export { clientOptions, createSmoodlyClient } from "./client.ts";
|
|
135
135
|
export { createSmoodlySite, pageContextOf } from "./site.ts";
|
package/dist/index.js
CHANGED
|
@@ -67,6 +67,6 @@ export const smoodly = {
|
|
|
67
67
|
marks,
|
|
68
68
|
};
|
|
69
69
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.js";
|
|
70
|
-
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig } from "./env.js";
|
|
70
|
+
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig, siteUrl } from "./env.js";
|
|
71
71
|
export { clientOptions, createSmoodlyClient } from "./client.js";
|
|
72
72
|
export { createSmoodlySite, pageContextOf } from "./site.js";
|
|
@@ -76,15 +76,19 @@ export function createPageRenderer(options) {
|
|
|
76
76
|
}
|
|
77
77
|
return _jsx(View, { entry: toEntryItem(hit.entry, hit.entry.fields, hit) });
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
79
|
+
/** The page's own address as its canonical, and hreflang from the SAME map a language switch reads
|
|
80
|
+
* (spec 2026-09-15 §3): the locales with a URL, through href. Next makes both absolute from the
|
|
81
|
+
* layout's `metadataBase` (tester run on 0.0.39, mission 6). */
|
|
82
|
+
function addresses(hit, locale) {
|
|
83
|
+
const own = hit.locales[locale]?.path;
|
|
84
|
+
const out = own ? { canonical: href(own, locale) } : {};
|
|
81
85
|
if (site.locales.supported.length < 2)
|
|
82
|
-
return
|
|
86
|
+
return out;
|
|
83
87
|
const languages = {};
|
|
84
|
-
for (const [
|
|
88
|
+
for (const [l, alt] of Object.entries(hit.locales))
|
|
85
89
|
if (alt?.path)
|
|
86
|
-
languages[
|
|
87
|
-
return {
|
|
90
|
+
languages[l] = href(alt.path, l);
|
|
91
|
+
return { ...out, languages };
|
|
88
92
|
}
|
|
89
93
|
// Next's cacheComponents serves the catch-all from a fallback shell that
|
|
90
94
|
// commits 200 before the page renders, so 404s and 308s are lost (spec
|
|
@@ -137,7 +141,13 @@ export function createPageRenderer(options) {
|
|
|
137
141
|
return {};
|
|
138
142
|
// A page type without seo: true still gets its page title as <title>; seo: true adds the panel's values on top.
|
|
139
143
|
const seo = hit.seo ?? (hit.kind === "page" ? { title: hit.title } : undefined);
|
|
140
|
-
|
|
144
|
+
const meta = metadataFromSeo(seo);
|
|
145
|
+
const { canonical, languages } = addresses(hit, locale);
|
|
146
|
+
// og:url only beside Open Graph values the page already sets: an openGraph of its own would
|
|
147
|
+
// replace the layout's defaults (Next merges metadata one key deep).
|
|
148
|
+
if (canonical && meta.openGraph)
|
|
149
|
+
meta.openGraph = { ...meta.openGraph, url: canonical };
|
|
150
|
+
return canonical || languages ? { ...meta, alternates: { ...(canonical ? { canonical } : {}), ...(languages ? { languages } : {}) } } : meta;
|
|
141
151
|
}
|
|
142
152
|
const any = () => true;
|
|
143
153
|
/** The escape hatch may only render ITS OWN registration's record. */
|
|
@@ -13,7 +13,9 @@ export async function previewRoute(request) {
|
|
|
13
13
|
}
|
|
14
14
|
else {
|
|
15
15
|
const secret = process.env.SMOODLY_PREVIEW_SECRET;
|
|
16
|
-
if (!secret
|
|
16
|
+
if (!secret)
|
|
17
|
+
return new Response("Draft links are off: SMOODLY_PREVIEW_SECRET is not set.", { status: 401 });
|
|
18
|
+
if (url.searchParams.get("secret") !== secret)
|
|
17
19
|
return new Response("Invalid secret", { status: 401 });
|
|
18
20
|
draft.enable();
|
|
19
21
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SMOODLY_VERSION = "0.0.
|
|
1
|
+
export declare const SMOODLY_VERSION = "0.0.41";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/write-version.mjs from package.json — do not edit.
|
|
2
|
-
export const SMOODLY_VERSION = "0.0.
|
|
2
|
+
export const SMOODLY_VERSION = "0.0.41";
|
package/package.json
CHANGED