emdash 0.7.0 → 0.8.0
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/{adapters-Di31kZ28.d.mts → adapters-BKSf3T9R.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-BKSf3T9R.d.mts.map} +1 -1
- package/dist/{apply-5uslYdUu.mjs → apply-x0eMK1lX.mjs} +18 -17
- package/dist/apply-x0eMK1lX.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +86 -15
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +22 -2
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +259 -71
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +16 -8
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-Chbr2GoP.mjs} +3 -3
- package/dist/byline-Chbr2GoP.mjs.map +1 -0
- package/dist/{bylines-hPTW79hw.mjs → bylines-CRNsVG88.mjs} +4 -4
- package/dist/{bylines-hPTW79hw.mjs.map → bylines-CRNsVG88.mjs.map} +1 -1
- package/dist/cli/index.mjs +16 -12
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{content-D7J5y73J.mjs → content-BcQPYxdV.mjs} +13 -15
- package/dist/content-BcQPYxdV.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-D0UT85nC.mjs → db-errors-l1Qh2RPR.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-l1Qh2RPR.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-DCVqE5ib.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-DCVqE5ib.mjs.map} +1 -1
- package/dist/{error-CiYn9yDu.mjs → error-zG5T1UGA.mjs} +1 -1
- package/dist/error-zG5T1UGA.mjs.map +1 -0
- package/dist/{index-De6_Xv3v.d.mts → index-DIb-CzNx.d.mts} +157 -14
- package/dist/index-DIb-CzNx.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +22 -20
- package/dist/{load-CBcmDIot.mjs → load-CyEoextb.mjs} +1 -1
- package/dist/{load-CBcmDIot.mjs.map → load-CyEoextb.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CndGj8kM.mjs} +8 -6
- package/dist/loader-CndGj8kM.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DH9xhc6t.mjs} +13 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-DqHVh136.mjs → media-D8FbNsl0.mjs} +4 -7
- package/dist/media-D8FbNsl0.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-BnAOqItE.mjs} +1 -1
- package/dist/mode-BnAOqItE.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/placeholder-C-fk5hYI.mjs.map +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-D29tWZ7o.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-D29tWZ7o.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-g4Ug-9j9.mjs → query-fqEdLFms.mjs} +9 -9
- package/dist/{query-g4Ug-9j9.mjs.map → query-fqEdLFms.mjs.map} +1 -1
- package/dist/{redirect-CN0Rt9Ob.mjs → redirect-D_pshWdf.mjs} +4 -4
- package/dist/redirect-D_pshWdf.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-C3Mr0ODu.mjs} +33 -9
- package/dist/registry-C3Mr0ODu.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-Ci7f5pBb.mjs} +1 -1
- package/dist/request-cache-Ci7f5pBb.mjs.map +1 -0
- package/dist/{runner-BR2xKwhn.d.mts → runner-OURCaApa.d.mts} +2 -2
- package/dist/{runner-BR2xKwhn.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-B0effn3j.mjs → search-BoZYFuUk.mjs} +227 -84
- package/dist/search-BoZYFuUk.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +12 -12
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-B4IAshV8.mjs} +5 -5
- package/dist/{taxonomies-K2z0Uhnj.mjs.map → taxonomies-B4IAshV8.mjs.map} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D9vnZqYS.mjs} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D9vnZqYS.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-C9ugt2Nr.mjs} +1 -1
- package/dist/{transport-BykRfpyy.mjs.map → transport-C9ugt2Nr.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-CUnEL3Vs.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-CUnEL3Vs.d.mts.map} +1 -1
- package/dist/types-BIgulNsW.mjs +68 -0
- package/dist/types-BIgulNsW.mjs.map +1 -0
- package/dist/{types-DDS4MxsT.mjs → types-Bm1dn-q3.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-Bm1dn-q3.mjs.map} +1 -1
- package/dist/{types-CnZYHyLW.d.mts → types-BmPPSUEx.d.mts} +1 -1
- package/dist/{types-CnZYHyLW.d.mts.map → types-BmPPSUEx.d.mts.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-BrA0xf5I.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-BrA0xf5I.d.mts.map} +1 -1
- package/dist/{types-C2v0c34j.d.mts → types-CS8FIX7L.d.mts} +1 -1
- package/dist/{types-C2v0c34j.d.mts.map → types-CS8FIX7L.d.mts.map} +1 -1
- package/dist/{types-BH2L167P.mjs → types-CgqmmMJB.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CgqmmMJB.mjs.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-DIMwPFub.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-DIMwPFub.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-i36XcA_X.d.mts} +49 -6
- package/dist/types-i36XcA_X.d.mts.map +1 -0
- package/dist/{validate-CqsNItbt.mjs → validate-CxVsLehf.mjs} +2 -2
- package/dist/{validate-CqsNItbt.mjs.map → validate-CxVsLehf.mjs.map} +1 -1
- package/dist/{validate-kM8Pjuf7.d.mts → validate-DHxmpFJt.d.mts} +4 -4
- package/dist/{validate-kM8Pjuf7.d.mts.map → validate-DHxmpFJt.d.mts.map} +1 -1
- package/dist/validation-C-ZpN2GI.mjs +144 -0
- package/dist/validation-C-ZpN2GI.mjs.map +1 -0
- package/dist/version-Bbq8TCrz.mjs +7 -0
- package/dist/{version-BnTKdfam.mjs.map → version-Bbq8TCrz.mjs.map} +1 -1
- package/dist/zod-generator-CpwccCIv.mjs +132 -0
- package/dist/zod-generator-CpwccCIv.mjs.map +1 -0
- package/package.json +18 -5
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +13 -0
- package/src/api/handlers/content.ts +122 -3
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +128 -16
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +6 -3
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/astro/integration/index.ts +13 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +19 -1
- package/src/astro/integration/virtual-modules.ts +41 -0
- package/src/astro/integration/vite-config.ts +43 -12
- package/src/astro/middleware/auth.ts +21 -0
- package/src/astro/middleware.ts +18 -1
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
- package/src/astro/routes/api/content/[collection]/index.ts +1 -9
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +10 -0
- package/src/astro/routes/api/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin.ts +8 -2
- package/src/astro/routes/api/setup/index.ts +2 -2
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +9 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/types.ts +114 -4
- package/src/cli/commands/bundle.ts +3 -1
- package/src/components/EmDashImage.astro +7 -6
- package/src/components/Gallery.astro +5 -3
- package/src/components/Image.astro +8 -3
- package/src/components/InlinePortableTextEditor.tsx +2 -1
- package/src/components/LiveSearch.astro +5 -14
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +40 -40
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +12 -16
- package/src/database/repositories/taxonomy.ts +14 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/emdash-runtime.ts +306 -90
- package/src/index.ts +5 -1
- package/src/loader.ts +6 -5
- package/src/mcp/server.ts +678 -105
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +11 -0
- package/src/plugins/manifest-schema.ts +12 -0
- package/src/plugins/types.ts +23 -2
- package/src/query.ts +1 -1
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +41 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/storage/s3.ts +12 -6
- package/src/virtual-modules.d.ts +21 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-5uslYdUu.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/content-D7J5y73J.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-De6_Xv3v.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/search-B0effn3j.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-BnTKdfam.mjs +0 -7
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level validation for content create / update.
|
|
3
|
+
*
|
|
4
|
+
* Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)
|
|
5
|
+
* into the handler boundary so REST and MCP both get the same enforcement:
|
|
6
|
+
*
|
|
7
|
+
* - required fields must be present and non-empty
|
|
8
|
+
* - select / multiSelect values must match the configured options
|
|
9
|
+
* - reference fields must resolve to a real, non-trashed target
|
|
10
|
+
*
|
|
11
|
+
* Errors surface as `{ code: "VALIDATION_ERROR", message }` with all
|
|
12
|
+
* offending fields listed in one message so callers can fix everything in
|
|
13
|
+
* a single round trip.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { sql, type Kysely } from "kysely";
|
|
17
|
+
|
|
18
|
+
import type { Database } from "../../database/types.js";
|
|
19
|
+
import { validateIdentifier } from "../../database/validate.js";
|
|
20
|
+
import { SchemaRegistry } from "../../schema/registry.js";
|
|
21
|
+
import type { Field } from "../../schema/types.js";
|
|
22
|
+
import { generateZodSchema } from "../../schema/zod-generator.js";
|
|
23
|
+
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
24
|
+
import { isMissingTableError } from "../../utils/db-errors.js";
|
|
25
|
+
|
|
26
|
+
type ValidationResult =
|
|
27
|
+
| { ok: true }
|
|
28
|
+
| { ok: false; error: { code: "VALIDATION_ERROR" | "COLLECTION_NOT_FOUND"; message: string } };
|
|
29
|
+
|
|
30
|
+
/** Treat `undefined`, `null`, and `""` as "not set". */
|
|
31
|
+
function isMissing(value: unknown): boolean {
|
|
32
|
+
return value === undefined || value === null || value === "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the target collection slug for a reference field.
|
|
37
|
+
*
|
|
38
|
+
* Schema-defined reference fields (the static `reference()` factory in
|
|
39
|
+
* `fields/reference.ts`) put the target in `options.collection`. The MCP
|
|
40
|
+
* `schema_create_field` tool also puts it there. Tests and some admin paths
|
|
41
|
+
* stash it inside `validation.collection` directly; we accept both.
|
|
42
|
+
*/
|
|
43
|
+
function getReferenceTargetCollection(field: Field): string | undefined {
|
|
44
|
+
const fromOptions = field.options?.collection;
|
|
45
|
+
if (typeof fromOptions === "string" && fromOptions.length > 0) return fromOptions;
|
|
46
|
+
const validation = field.validation;
|
|
47
|
+
if (validation && "collection" in validation) {
|
|
48
|
+
const fromValidation: unknown = (validation as { collection?: unknown }).collection;
|
|
49
|
+
if (typeof fromValidation === "string" && fromValidation.length > 0) return fromValidation;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format a Zod issue path into a human-readable field reference, e.g.
|
|
56
|
+
* `tags`, `tags.1`, `image.alt`.
|
|
57
|
+
*/
|
|
58
|
+
function formatIssuePath(path: ReadonlyArray<PropertyKey>): string {
|
|
59
|
+
if (path.length === 0) return "(root)";
|
|
60
|
+
return path.map((seg) => String(seg)).join(".");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate `data` against the collection's field definitions.
|
|
65
|
+
*
|
|
66
|
+
* `partial: true` switches Zod into partial mode so updates can include
|
|
67
|
+
* only the fields being changed without tripping required-field errors on
|
|
68
|
+
* fields the caller didn't touch. Required fields that ARE present in
|
|
69
|
+
* partial-mode data still get the empty-string check below.
|
|
70
|
+
*/
|
|
71
|
+
export async function validateContentData(
|
|
72
|
+
db: Kysely<Database>,
|
|
73
|
+
collection: string,
|
|
74
|
+
data: Record<string, unknown>,
|
|
75
|
+
options: { partial?: boolean } = {},
|
|
76
|
+
): Promise<ValidationResult> {
|
|
77
|
+
const registry = new SchemaRegistry(db);
|
|
78
|
+
const collectionWithFields = await registry.getCollectionWithFields(collection);
|
|
79
|
+
if (!collectionWithFields) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: {
|
|
83
|
+
code: "COLLECTION_NOT_FOUND",
|
|
84
|
+
message: `Collection '${collection}' not found`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const issues: string[] = [];
|
|
90
|
+
|
|
91
|
+
// Detect unknown keys explicitly so callers get a useful error rather
|
|
92
|
+
// than silently dropped data. Leading-underscore keys (e.g. `_slug`,
|
|
93
|
+
// `_rev`) are reserved for internal handler/runtime use and aren't real
|
|
94
|
+
// fields; skip them.
|
|
95
|
+
const knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));
|
|
96
|
+
for (const key of Object.keys(data)) {
|
|
97
|
+
if (key.startsWith("_")) continue;
|
|
98
|
+
if (!knownFields.has(key)) {
|
|
99
|
+
issues.push(`${key}: unknown field on collection '${collection}'`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Zod handles type, enum, length and missing-required (in non-partial
|
|
104
|
+
// mode) checks. Empty-string handling for required string fields is
|
|
105
|
+
// done as a separate pass below since Zod's `z.string()` accepts "".
|
|
106
|
+
const baseSchema = generateZodSchema(collectionWithFields);
|
|
107
|
+
const schema = options.partial ? baseSchema.partial() : baseSchema;
|
|
108
|
+
const parsed = schema.safeParse(data);
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
for (const issue of parsed.error.issues) {
|
|
111
|
+
issues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Empty-string-on-required check. In create mode (partial=false) Zod
|
|
116
|
+
// already catches missing/null for required fields, but `z.string()`
|
|
117
|
+
// happily accepts "". In update mode (partial=true) the field is only
|
|
118
|
+
// checked if it's present in `data`.
|
|
119
|
+
for (const field of collectionWithFields.fields) {
|
|
120
|
+
if (!field.required) continue;
|
|
121
|
+
const present = Object.hasOwn(data, field.slug);
|
|
122
|
+
if (options.partial && !present) continue;
|
|
123
|
+
if (data[field.slug] === "") {
|
|
124
|
+
issues.push(`${field.slug}: required (empty value not allowed)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Reference target existence. Only check fields that:
|
|
129
|
+
// - have a value (non-missing) in `data`
|
|
130
|
+
// - have a resolvable target collection
|
|
131
|
+
// - in partial mode: are present in `data`
|
|
132
|
+
// Batch one IN-query per target collection to keep round-trips low.
|
|
133
|
+
const refsByTarget = new Map<string, { field: string; id: string }[]>();
|
|
134
|
+
for (const field of collectionWithFields.fields) {
|
|
135
|
+
if (field.type !== "reference") continue;
|
|
136
|
+
if (options.partial && !Object.hasOwn(data, field.slug)) continue;
|
|
137
|
+
const value = data[field.slug];
|
|
138
|
+
if (isMissing(value)) continue;
|
|
139
|
+
if (typeof value !== "string") continue; // Zod will have flagged this already
|
|
140
|
+
const target = getReferenceTargetCollection(field);
|
|
141
|
+
if (!target) continue;
|
|
142
|
+
const list = refsByTarget.get(target) ?? [];
|
|
143
|
+
list.push({ field: field.slug, id: value });
|
|
144
|
+
refsByTarget.set(target, list);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const [target, refs] of refsByTarget) {
|
|
148
|
+
// Validate the target collection slug before interpolating into raw
|
|
149
|
+
// SQL — defense-in-depth even though slugs are already validated at
|
|
150
|
+
// schema-create time.
|
|
151
|
+
try {
|
|
152
|
+
validateIdentifier(target, "reference target collection");
|
|
153
|
+
} catch {
|
|
154
|
+
for (const ref of refs) {
|
|
155
|
+
issues.push(`${ref.field}: invalid reference target collection '${target}'`);
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const ids = [...new Set(refs.map((r) => r.id))];
|
|
161
|
+
const tableName = `ec_${target}`;
|
|
162
|
+
|
|
163
|
+
// Chunk the IN clause to stay below D1's bind-parameter limit. One
|
|
164
|
+
// reference per request is the common case today; chunking makes the
|
|
165
|
+
// helper safe if a future multiSelect-of-references is added.
|
|
166
|
+
const found = new Set<string>();
|
|
167
|
+
let targetTableMissing = false;
|
|
168
|
+
for (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {
|
|
169
|
+
try {
|
|
170
|
+
const rows = await sql<{ id: string }>`
|
|
171
|
+
SELECT id FROM ${sql.ref(tableName)}
|
|
172
|
+
WHERE id IN (${sql.join(idChunk)})
|
|
173
|
+
AND deleted_at IS NULL
|
|
174
|
+
`.execute(db);
|
|
175
|
+
for (const row of rows.rows) {
|
|
176
|
+
found.add(row.id);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Missing table = the target collection table doesn't exist
|
|
180
|
+
// (orphan reference). Treat all those references as missing.
|
|
181
|
+
// Any other DB error (permissions, connection, syntax) must
|
|
182
|
+
// propagate — silently dropping data integrity errors as
|
|
183
|
+
// "not found" is exactly the bug F5 fixes.
|
|
184
|
+
if (isMissingTableError(error)) {
|
|
185
|
+
targetTableMissing = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (targetTableMissing) {
|
|
192
|
+
for (const ref of refs) {
|
|
193
|
+
issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const ref of refs) {
|
|
198
|
+
if (!found.has(ref.id)) {
|
|
199
|
+
issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (issues.length === 0) return { ok: true };
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
error: {
|
|
208
|
+
code: "VALIDATION_ERROR",
|
|
209
|
+
message: issues.join("; "),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -122,6 +122,7 @@ import {
|
|
|
122
122
|
reorderWidgetsBody,
|
|
123
123
|
updateWidgetBody,
|
|
124
124
|
widgetAreaSchema,
|
|
125
|
+
widgetAreaWithWidgetsAndCountSchema,
|
|
125
126
|
widgetAreaWithWidgetsSchema,
|
|
126
127
|
widgetSchema,
|
|
127
128
|
} from "../schemas/widgets.js";
|
|
@@ -1581,7 +1582,9 @@ const widgetPaths = {
|
|
|
1581
1582
|
description: "Widget area list",
|
|
1582
1583
|
content: {
|
|
1583
1584
|
[JSON_CONTENT]: {
|
|
1584
|
-
schema: successEnvelope(
|
|
1585
|
+
schema: successEnvelope(
|
|
1586
|
+
z.object({ items: z.array(widgetAreaWithWidgetsAndCountSchema) }),
|
|
1587
|
+
),
|
|
1585
1588
|
},
|
|
1586
1589
|
},
|
|
1587
1590
|
},
|
package/src/api/public-url.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* Workers-safe: no Node.js imports.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** Minimal config shape — avoids importing the full EmDashConfig type tree. */
|
|
13
|
+
interface SiteUrlConfig {
|
|
14
|
+
siteUrl?: string;
|
|
15
|
+
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Resolve siteUrl from runtime environment variables.
|
|
@@ -67,7 +70,7 @@ function getEnvSiteUrl(): string | undefined {
|
|
|
67
70
|
* @param config The EmDash config (from `locals.emdash?.config`)
|
|
68
71
|
* @returns Origin string, e.g. `"https://mysite.example.com"`
|
|
69
72
|
*/
|
|
70
|
-
export function getPublicOrigin(url: URL, config?:
|
|
73
|
+
export function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {
|
|
71
74
|
return config?.siteUrl || getEnvSiteUrl() || url.origin;
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -79,6 +82,6 @@ export function getPublicOrigin(url: URL, config?: EmDashConfig): string {
|
|
|
79
82
|
* @param path Path to append (must start with `/`)
|
|
80
83
|
* @returns Full URL string, e.g. `"https://mysite.example.com/_emdash/admin/login"`
|
|
81
84
|
*/
|
|
82
|
-
export function getPublicUrl(url: URL, config:
|
|
85
|
+
export function getPublicUrl(url: URL, config: SiteUrlConfig | undefined, path: string): string {
|
|
83
86
|
return `${getPublicOrigin(url, config)}${path}`;
|
|
84
87
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API route utilities for auth provider routes.
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports the utilities that auth provider route handlers
|
|
5
|
+
* need from core. Auth providers (plugins) import these via `emdash/api/route-utils`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { apiError, apiSuccess, handleError } from "./error.js";
|
|
9
|
+
export { parseBody, parseQuery, isParseError } from "./parse.js";
|
|
10
|
+
export type { ParseResult } from "./parse.js";
|
|
11
|
+
export { finalizeSetup } from "./setup-complete.js";
|
|
12
|
+
export { OptionsRepository } from "../database/repositories/options.js";
|
|
13
|
+
export { getAuthProviderStorage } from "./auth-storage.js";
|
|
14
|
+
export { getPublicOrigin } from "./public-url.js";
|
|
@@ -22,7 +22,7 @@ export const roleLevel = z.coerce
|
|
|
22
22
|
/** Pagination query params — cursor-based */
|
|
23
23
|
export const cursorPaginationQuery = z
|
|
24
24
|
.object({
|
|
25
|
-
cursor: z.string().optional().meta({ description: "Opaque cursor for pagination" }),
|
|
25
|
+
cursor: z.string().max(2048).optional().meta({ description: "Opaque cursor for pagination" }),
|
|
26
26
|
limit: z.coerce.number().int().min(1).max(100).optional().default(50).meta({
|
|
27
27
|
description: "Maximum number of items to return (1-100, default 50)",
|
|
28
28
|
}),
|
package/src/api/schemas/setup.ts
CHANGED
|
@@ -35,3 +35,11 @@ export const setupAdminBody = z.object({
|
|
|
35
35
|
export const setupAdminVerifyBody = z.object({
|
|
36
36
|
credential: registrationCredential,
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
export const atprotoLoginBody = z.object({
|
|
40
|
+
handle: z.string().trim().min(1),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const setupAtprotoAdminBody = z.object({
|
|
44
|
+
handle: z.string().trim().min(1),
|
|
45
|
+
});
|
|
@@ -60,16 +60,12 @@ export const widgetAreaSchema = z
|
|
|
60
60
|
export const widgetSchema = z
|
|
61
61
|
.object({
|
|
62
62
|
id: z.string(),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
component_props: z.string().nullable(),
|
|
70
|
-
sort_order: z.number().int(),
|
|
71
|
-
created_at: z.string(),
|
|
72
|
-
updated_at: z.string(),
|
|
63
|
+
type: widgetType,
|
|
64
|
+
title: z.string().optional(),
|
|
65
|
+
content: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
66
|
+
menuName: z.string().optional(),
|
|
67
|
+
componentId: z.string().optional(),
|
|
68
|
+
componentProps: z.record(z.string(), z.unknown()).optional(),
|
|
73
69
|
})
|
|
74
70
|
.meta({ id: "Widget" });
|
|
75
71
|
|
|
@@ -78,3 +74,9 @@ export const widgetAreaWithWidgetsSchema = widgetAreaSchema
|
|
|
78
74
|
widgets: z.array(widgetSchema),
|
|
79
75
|
})
|
|
80
76
|
.meta({ id: "WidgetAreaWithWidgets" });
|
|
77
|
+
|
|
78
|
+
export const widgetAreaWithWidgetsAndCountSchema = widgetAreaWithWidgetsSchema
|
|
79
|
+
.extend({
|
|
80
|
+
widgetCount: z.number().int(),
|
|
81
|
+
})
|
|
82
|
+
.meta({ id: "WidgetAreaWithWidgetsAndCount" });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared setup completion logic.
|
|
3
|
+
*
|
|
4
|
+
* Called by OAuth callbacks and the passkey verify step when the first user
|
|
5
|
+
* is created during setup. Persists site title/tagline from setup state
|
|
6
|
+
* and marks setup as complete.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Kysely } from "kysely";
|
|
10
|
+
|
|
11
|
+
import { OptionsRepository } from "../database/repositories/options.js";
|
|
12
|
+
import type { Database } from "../database/types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Finalize setup after the first admin user is created.
|
|
16
|
+
*
|
|
17
|
+
* Reads the setup_state option (written by the setup wizard's step 1),
|
|
18
|
+
* persists site_title and site_tagline, then marks setup complete.
|
|
19
|
+
*
|
|
20
|
+
* Safe to call multiple times — checks setup_complete first and no-ops
|
|
21
|
+
* if already done.
|
|
22
|
+
*/
|
|
23
|
+
export async function finalizeSetup(db: Kysely<Database>): Promise<void> {
|
|
24
|
+
const options = new OptionsRepository(db);
|
|
25
|
+
|
|
26
|
+
const setupComplete = await options.get("emdash:setup_complete");
|
|
27
|
+
if (setupComplete === true || setupComplete === "true") return;
|
|
28
|
+
|
|
29
|
+
// Persist site title/tagline from setup state (stored in step 1)
|
|
30
|
+
const setupState = await options.get<Record<string, unknown>>("emdash:setup_state");
|
|
31
|
+
if (setupState?.title && typeof setupState.title === "string") {
|
|
32
|
+
await options.set("emdash:site_title", setupState.title);
|
|
33
|
+
}
|
|
34
|
+
if (setupState?.tagline && typeof setupState.tagline === "string") {
|
|
35
|
+
await options.set("emdash:site_tagline", setupState.tagline);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await options.set("emdash:setup_complete", true);
|
|
39
|
+
await options.delete("emdash:setup_state");
|
|
40
|
+
}
|
|
@@ -15,7 +15,12 @@ import type { AstroIntegration, AstroIntegrationLogger } from "astro";
|
|
|
15
15
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
16
16
|
import { local } from "../storage/adapters.js";
|
|
17
17
|
import { notoSans } from "./font-provider.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
injectCoreRoutes,
|
|
20
|
+
injectBuiltinAuthRoutes,
|
|
21
|
+
injectAuthProviderRoutes,
|
|
22
|
+
injectMcpRoute,
|
|
23
|
+
} from "./routes.js";
|
|
19
24
|
import type { EmDashConfig, PluginDescriptor } from "./runtime.js";
|
|
20
25
|
import { createViteConfig } from "./vite-config.js";
|
|
21
26
|
|
|
@@ -157,6 +162,7 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
157
162
|
database: resolvedConfig.database,
|
|
158
163
|
storage: resolvedConfig.storage,
|
|
159
164
|
auth: resolvedConfig.auth,
|
|
165
|
+
authProviders: resolvedConfig.authProviders,
|
|
160
166
|
marketplace: resolvedConfig.marketplace,
|
|
161
167
|
siteUrl: resolvedConfig.siteUrl,
|
|
162
168
|
trustedProxyHeaders: resolvedConfig.trustedProxyHeaders,
|
|
@@ -267,7 +273,12 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
267
273
|
// Inject all core routes
|
|
268
274
|
injectCoreRoutes(injectRoute);
|
|
269
275
|
|
|
270
|
-
//
|
|
276
|
+
// Inject routes from pluggable auth providers (authProviders config)
|
|
277
|
+
if (resolvedConfig.authProviders?.length) {
|
|
278
|
+
injectAuthProviderRoutes(injectRoute, resolvedConfig.authProviders);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Inject passkey/oauth/magic-link routes unless transparent external auth is active
|
|
271
282
|
if (!useExternalAuth) {
|
|
272
283
|
injectBuiltinAuthRoutes(injectRoute);
|
|
273
284
|
}
|
|
@@ -46,6 +46,12 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
46
46
|
entrypoint: resolveRoute("api/manifest.ts"),
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// Auth mode endpoint (public — used by the login page to pick the right UI)
|
|
50
|
+
injectRoute({
|
|
51
|
+
pattern: "/_emdash/api/auth/mode",
|
|
52
|
+
entrypoint: resolveRoute("api/auth/mode.ts"),
|
|
53
|
+
});
|
|
54
|
+
|
|
49
55
|
injectRoute({
|
|
50
56
|
pattern: "/_emdash/api/dashboard",
|
|
51
57
|
entrypoint: resolveRoute("api/dashboard.ts"),
|
|
@@ -747,6 +753,28 @@ export function injectMcpRoute(injectRoute: InjectRoute): void {
|
|
|
747
753
|
});
|
|
748
754
|
}
|
|
749
755
|
|
|
756
|
+
/**
|
|
757
|
+
* Injects routes from pluggable auth providers.
|
|
758
|
+
*
|
|
759
|
+
* Each provider declares the routes it needs in its `AuthProviderDescriptor.routes` array.
|
|
760
|
+
* Routes are injected at build time so Vite can bundle them.
|
|
761
|
+
*/
|
|
762
|
+
export function injectAuthProviderRoutes(
|
|
763
|
+
injectRoute: InjectRoute,
|
|
764
|
+
providers: Array<{ routes?: Array<{ pattern: string; entrypoint: string }> }>,
|
|
765
|
+
): void {
|
|
766
|
+
for (const provider of providers) {
|
|
767
|
+
if (provider.routes) {
|
|
768
|
+
for (const route of provider.routes) {
|
|
769
|
+
injectRoute({
|
|
770
|
+
pattern: route.pattern,
|
|
771
|
+
entrypoint: route.entrypoint,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
750
778
|
/**
|
|
751
779
|
* Injects passkey/oauth/magic-link auth routes.
|
|
752
780
|
* Only used when NOT using external auth.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* DO NOT import Node.js-only modules here (fs, path, module, etc.)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { AuthDescriptor } from "../../auth/types.js";
|
|
10
|
+
import type { AuthDescriptor, AuthProviderDescriptor } from "../../auth/types.js";
|
|
11
11
|
import type { DatabaseDescriptor } from "../../db/adapters.js";
|
|
12
12
|
import type { MediaProviderDescriptor } from "../../media/types.js";
|
|
13
13
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
@@ -222,6 +222,24 @@ export interface EmDashConfig {
|
|
|
222
222
|
*/
|
|
223
223
|
auth?: AuthDescriptor;
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Pluggable auth providers (login methods on the login page).
|
|
227
|
+
*
|
|
228
|
+
* Auth providers appear as options alongside passkey on the login page
|
|
229
|
+
* and setup wizard. Any provider can be used to create the initial
|
|
230
|
+
* admin account. Passkey is built-in; providers listed here are additive.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* import { atproto } from "@emdash-cms/auth-atproto";
|
|
235
|
+
*
|
|
236
|
+
* emdash({
|
|
237
|
+
* authProviders: [atproto()],
|
|
238
|
+
* })
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
authProviders?: AuthProviderDescriptor[];
|
|
242
|
+
|
|
225
243
|
/**
|
|
226
244
|
* MCP (Model Context Protocol) server endpoint.
|
|
227
245
|
*
|
|
@@ -10,6 +10,7 @@ import { readFileSync } from "node:fs";
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
|
|
13
|
+
import type { AuthProviderDescriptor } from "../../auth/types.js";
|
|
13
14
|
import type { MediaProviderDescriptor } from "../../media/types.js";
|
|
14
15
|
import { defaultSeed } from "../../seed/default.js";
|
|
15
16
|
import type { PluginDescriptor } from "./runtime.js";
|
|
@@ -47,6 +48,9 @@ export const RESOLVED_VIRTUAL_SANDBOXED_PLUGINS_ID = "\0" + VIRTUAL_SANDBOXED_PL
|
|
|
47
48
|
export const VIRTUAL_AUTH_ID = "virtual:emdash/auth";
|
|
48
49
|
export const RESOLVED_VIRTUAL_AUTH_ID = "\0" + VIRTUAL_AUTH_ID;
|
|
49
50
|
|
|
51
|
+
export const VIRTUAL_AUTH_PROVIDERS_ID = "virtual:emdash/auth-providers";
|
|
52
|
+
export const RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID = "\0" + VIRTUAL_AUTH_PROVIDERS_ID;
|
|
53
|
+
|
|
50
54
|
export const VIRTUAL_MEDIA_PROVIDERS_ID = "virtual:emdash/media-providers";
|
|
51
55
|
export const RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID = "\0" + VIRTUAL_MEDIA_PROVIDERS_ID;
|
|
52
56
|
|
|
@@ -135,6 +139,43 @@ export const authenticate = _authenticate;
|
|
|
135
139
|
`;
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Generates the auth providers module.
|
|
144
|
+
*
|
|
145
|
+
* Statically imports each auth provider's `adminEntry` module and exports
|
|
146
|
+
* a registry keyed by provider ID. The admin UI uses this to render
|
|
147
|
+
* provider-specific login buttons/forms and setup steps.
|
|
148
|
+
*
|
|
149
|
+
* Follows the same pattern as `generateAdminRegistryModule()` for plugins.
|
|
150
|
+
*/
|
|
151
|
+
export function generateAuthProvidersModule(descriptors: AuthProviderDescriptor[]): string {
|
|
152
|
+
const withAdmin = descriptors.filter((d) => d.adminEntry);
|
|
153
|
+
|
|
154
|
+
if (withAdmin.length === 0) {
|
|
155
|
+
return `export const authProviders = {};`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const imports: string[] = [];
|
|
159
|
+
const entries: string[] = [];
|
|
160
|
+
|
|
161
|
+
withAdmin.forEach((descriptor, index) => {
|
|
162
|
+
const varName = `authProvider${index}`;
|
|
163
|
+
imports.push(`import * as ${varName} from ${JSON.stringify(descriptor.adminEntry)};`);
|
|
164
|
+
entries.push(
|
|
165
|
+
` ${JSON.stringify(descriptor.id)}: { ...${varName}, id: ${JSON.stringify(descriptor.id)}, label: ${JSON.stringify(descriptor.label)} },`,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return `
|
|
170
|
+
// Auto-generated auth provider registry
|
|
171
|
+
${imports.join("\n")}
|
|
172
|
+
|
|
173
|
+
export const authProviders = {
|
|
174
|
+
${entries.join("\n")}
|
|
175
|
+
};
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
|
|
138
179
|
/**
|
|
139
180
|
* Generates the plugins module.
|
|
140
181
|
* Imports and instantiates all plugins at runtime.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
|
-
import { dirname, resolve } from "node:path";
|
|
10
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
|
|
13
13
|
import type { AstroConfig } from "astro";
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
RESOLVED_VIRTUAL_SANDBOXED_PLUGINS_ID,
|
|
33
33
|
VIRTUAL_AUTH_ID,
|
|
34
34
|
RESOLVED_VIRTUAL_AUTH_ID,
|
|
35
|
+
VIRTUAL_AUTH_PROVIDERS_ID,
|
|
36
|
+
RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID,
|
|
35
37
|
VIRTUAL_MEDIA_PROVIDERS_ID,
|
|
36
38
|
RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID,
|
|
37
39
|
VIRTUAL_BLOCK_COMPONENTS_ID,
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
generateDialectModule,
|
|
47
49
|
generateStorageModule,
|
|
48
50
|
generateAuthModule,
|
|
51
|
+
generateAuthProvidersModule,
|
|
49
52
|
generatePluginsModule,
|
|
50
53
|
generateAdminRegistryModule,
|
|
51
54
|
generateSandboxRunnerModule,
|
|
@@ -104,24 +107,34 @@ function resolveAdminDist(): string {
|
|
|
104
107
|
return dirname(adminPath);
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Check whether child is inside parent without relying on simple prefix checks.
|
|
112
|
+
*/
|
|
113
|
+
function isInside(parent: string, child: string): boolean {
|
|
114
|
+
const relativePath = relative(parent, child);
|
|
115
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
/**
|
|
108
119
|
* Resolve path to the admin package source directory.
|
|
109
|
-
* In dev mode, we alias @emdash-cms/admin to the source so
|
|
110
|
-
* directly — giving instant HMR instead of requiring a
|
|
120
|
+
* In dev mode inside this repo, we alias @emdash-cms/admin to the source so
|
|
121
|
+
* Vite processes it directly — giving instant HMR instead of requiring a
|
|
122
|
+
* rebuild + restart. External apps should use the built package surface.
|
|
111
123
|
*/
|
|
112
|
-
function resolveAdminSource(): string | undefined {
|
|
124
|
+
function resolveAdminSource(projectRoot: string): string | undefined {
|
|
113
125
|
const require = createRequire(import.meta.url);
|
|
114
126
|
const adminPath = require.resolve("@emdash-cms/admin");
|
|
115
127
|
// dist/index.js -> go up to package root, then into src/
|
|
116
128
|
const packageRoot = resolve(dirname(adminPath), "..");
|
|
129
|
+
const repoRoot = resolve(packageRoot, "..", "..");
|
|
117
130
|
const srcEntry = resolve(packageRoot, "src", "index.ts");
|
|
118
131
|
|
|
119
132
|
try {
|
|
120
|
-
if (existsSync(srcEntry)) {
|
|
133
|
+
if (existsSync(srcEntry) && isInside(repoRoot, projectRoot)) {
|
|
121
134
|
return resolve(packageRoot, "src");
|
|
122
135
|
}
|
|
123
136
|
} catch {
|
|
124
|
-
// Not in
|
|
137
|
+
// Not in local repo — fall back to dist
|
|
125
138
|
}
|
|
126
139
|
return undefined;
|
|
127
140
|
}
|
|
@@ -170,6 +183,9 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
170
183
|
if (id === VIRTUAL_AUTH_ID) {
|
|
171
184
|
return RESOLVED_VIRTUAL_AUTH_ID;
|
|
172
185
|
}
|
|
186
|
+
if (id === VIRTUAL_AUTH_PROVIDERS_ID) {
|
|
187
|
+
return RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID;
|
|
188
|
+
}
|
|
173
189
|
if (id === VIRTUAL_MEDIA_PROVIDERS_ID) {
|
|
174
190
|
return RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID;
|
|
175
191
|
}
|
|
@@ -228,6 +244,10 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
228
244
|
}
|
|
229
245
|
return generateAuthModule(authDescriptor.entrypoint);
|
|
230
246
|
}
|
|
247
|
+
// Generate auth providers module (pluggable login methods)
|
|
248
|
+
if (id === RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID) {
|
|
249
|
+
return generateAuthProvidersModule(resolvedConfig.authProviders ?? []);
|
|
250
|
+
}
|
|
231
251
|
// Generate media providers module
|
|
232
252
|
if (id === RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID) {
|
|
233
253
|
return generateMediaProvidersModule(resolvedConfig.mediaProviders ?? []);
|
|
@@ -281,12 +301,9 @@ export function createViteConfig(
|
|
|
281
301
|
const adminDistPath = resolveAdminDist();
|
|
282
302
|
const cloudflare = isCloudflareAdapter(options.astroConfig);
|
|
283
303
|
const isDev = command === "dev";
|
|
304
|
+
const projectRoot = fileURLToPath(options.astroConfig.root);
|
|
284
305
|
|
|
285
|
-
|
|
286
|
-
// CSS always comes from dist/ (pre-compiled by @tailwindcss/cli) since Tailwind's
|
|
287
|
-
// Vite plugin has native deps that don't bundle well. Run `pnpm dev` in packages/admin
|
|
288
|
-
// alongside the demo server to get CSS watch-rebuilds too.
|
|
289
|
-
const adminSourcePath = isDev ? resolveAdminSource() : undefined;
|
|
306
|
+
const adminSourcePath = isDev ? resolveAdminSource(projectRoot) : undefined;
|
|
290
307
|
const useSource = adminSourcePath !== undefined;
|
|
291
308
|
|
|
292
309
|
return {
|
|
@@ -308,6 +325,20 @@ export function createViteConfig(
|
|
|
308
325
|
alias: [
|
|
309
326
|
{ find: "@emdash-cms/admin/styles.css", replacement: resolve(adminDistPath, "styles.css") },
|
|
310
327
|
{ find: "@emdash-cms/admin", replacement: useSource ? adminSourcePath : adminDistPath },
|
|
328
|
+
// `use-sync-external-store/shim` is a React <18 polyfill that ships
|
|
329
|
+
// only as CJS. It's pulled in transitively by `@tiptap/react`. With
|
|
330
|
+
// pnpm's virtual store the file lives under .pnpm/, where Vite's
|
|
331
|
+
// dep scanner can't reach it for pre-bundling — so the browser is
|
|
332
|
+
// served raw `module.exports` and hydration fails with
|
|
333
|
+
// `SyntaxError: ... does not provide an export named
|
|
334
|
+
// 'useSyncExternalStore'`. Redirect both shim entry points to the
|
|
335
|
+
// main `use-sync-external-store` package, which on React >=18
|
|
336
|
+
// (our peer-dep floor) delegates to React's built-in hook.
|
|
337
|
+
{
|
|
338
|
+
find: "use-sync-external-store/shim/index.js",
|
|
339
|
+
replacement: "use-sync-external-store",
|
|
340
|
+
},
|
|
341
|
+
{ find: "use-sync-external-store/shim", replacement: "use-sync-external-store" },
|
|
311
342
|
],
|
|
312
343
|
},
|
|
313
344
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Monorepo has both vite 6 (docs) and vite 7 (core). tsgo resolves correctly.
|
|
@@ -316,7 +347,7 @@ export function createViteConfig(
|
|
|
316
347
|
// In dev mode with source alias, compile Lingui macros on the fly
|
|
317
348
|
// and redirect locale .mjs imports to dist/.
|
|
318
349
|
// In production, macros are pre-compiled by tsdown in the admin package.
|
|
319
|
-
...(useSource ? [linguiMacroPlugin(adminSourcePath
|
|
350
|
+
...(useSource ? [linguiMacroPlugin(adminSourcePath, adminDistPath)] : []),
|
|
320
351
|
] as NonNullable<AstroConfig["vite"]>["plugins"],
|
|
321
352
|
// Handle native modules for SSR.
|
|
322
353
|
// On Node: external keeps native addons out of the SSR bundle.
|