emdash 0.4.0 → 0.6.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-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-B4MsLM-w.mjs} +27 -12
- package/dist/apply-B4MsLM-w.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 +208 -34
- 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 +34 -9
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +5 -3
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +460 -180
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +8 -8
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +9 -8
- 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/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs.map +1 -1
- 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 +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CRg3PWfZ.d.mts → index-BYv0mB9g.d.mts} +135 -19
- package/dist/index-BYv0mB9g.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +20 -18
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.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-B6Vu0d2i.mjs → query-Bk_3vKvU.mjs} +78 -11
- package/dist/query-Bk_3vKvU.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-Fl2NcUUz.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-Fl2NcUUz.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-B5p9D36n.mjs → search-DI4bM2w9.mjs} +110 -209
- package/dist/search-DI4bM2w9.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +8 -7
- 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.mjs +1 -1
- package/dist/taxonomies-DbrKzDju.mjs +308 -0
- package/dist/taxonomies-DbrKzDju.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-8xrvl_68.d.mts} +1 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-8xrvl_68.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-gLYVCXCQ.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-BYWYxLcp.d.mts → types-DgrIP0tF.d.mts} +9 -2
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-CcNRWH6I.d.mts → validate-CaLH1Ia2.d.mts} +5 -52
- package/dist/validate-CaLH1Ia2.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/version-Uaf2ynPX.mjs +7 -0
- package/dist/{version-DlTDRdpv.mjs.map → version-Uaf2ynPX.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/auth.ts +7 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +176 -0
- package/src/astro/integration/index.ts +42 -0
- package/src/astro/integration/routes.ts +17 -1
- package/src/astro/integration/runtime.ts +63 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +39 -6
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +10 -5
- package/src/astro/routes/api/auth/invite/register-options.ts +78 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +1 -1
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- package/src/astro/routes/api/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +6 -4
- package/src/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/components/Table.astro +73 -41
- package/src/components/index.ts +2 -12
- package/src/components/marks.ts +20 -0
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +1 -0
- package/src/plugins/email-console.ts +9 -2
- package/src/plugins/types.ts +8 -0
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +15 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CRg3PWfZ.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/search-B5p9D36n.mjs.map +0 -1
- package/dist/types-BYWYxLcp.d.mts.map +0 -1
- package/dist/types-gLYVCXCQ.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-CcNRWH6I.d.mts.map +0 -1
- package/dist/version-DlTDRdpv.mjs +0 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-
|
|
1
|
+
{"version":3,"file":"version-Uaf2ynPX.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Build-time version constants, replaced by tsdown/Vite `define`.\n * Falls back to \"dev\" when running uncompiled (tests, dev).\n */\n\ndeclare const __EMDASH_VERSION__: string;\ndeclare const __EMDASH_COMMIT__: string;\n\nexport const VERSION: string =\n\ttypeof __EMDASH_VERSION__ !== \"undefined\" ? __EMDASH_VERSION__ : \"dev\";\n\nexport const COMMIT: string = typeof __EMDASH_COMMIT__ !== \"undefined\" ? __EMDASH_COMMIT__ : \"dev\";\n"],"mappings":";AAQA,MAAa;AAGb,MAAa"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emdash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Astro-native CMS with WordPress migration support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
"types": "./dist/db/postgres.d.mts",
|
|
62
62
|
"default": "./dist/db/postgres.mjs"
|
|
63
63
|
},
|
|
64
|
+
"./database/instrumentation": {
|
|
65
|
+
"types": "./dist/database/instrumentation.d.mts",
|
|
66
|
+
"default": "./dist/database/instrumentation.mjs"
|
|
67
|
+
},
|
|
64
68
|
"./storage/local": {
|
|
65
69
|
"types": "./dist/storage/local.d.mts",
|
|
66
70
|
"default": "./dist/storage/local.mjs"
|
|
@@ -142,6 +146,7 @@
|
|
|
142
146
|
"#mcp/*": "./src/mcp/*",
|
|
143
147
|
"#comments/*": "./src/comments/*",
|
|
144
148
|
"#bylines/*": "./src/bylines/*",
|
|
149
|
+
"#taxonomies/*": "./src/taxonomies/*",
|
|
145
150
|
"#redirects/*": "./src/redirects/*",
|
|
146
151
|
"#types": "./src/astro/types.js"
|
|
147
152
|
},
|
|
@@ -180,9 +185,9 @@
|
|
|
180
185
|
"ulidx": "^2.4.1",
|
|
181
186
|
"upng-js": "^2.1.0",
|
|
182
187
|
"zod": "^4.3.5",
|
|
183
|
-
"@emdash-cms/
|
|
184
|
-
"@emdash-cms/
|
|
185
|
-
"@emdash-cms/gutenberg-to-portable-text": "0.
|
|
188
|
+
"@emdash-cms/auth": "0.6.0",
|
|
189
|
+
"@emdash-cms/admin": "0.6.0",
|
|
190
|
+
"@emdash-cms/gutenberg-to-portable-text": "0.6.0"
|
|
186
191
|
},
|
|
187
192
|
"optionalDependencies": {
|
|
188
193
|
"@libsql/kysely-libsql": "^0.4.0",
|
|
@@ -210,7 +215,7 @@
|
|
|
210
215
|
"vite": "^6.0.0",
|
|
211
216
|
"vitest": "^4.0.18",
|
|
212
217
|
"zod-openapi": "^5.4.6",
|
|
213
|
-
"@emdash-cms/blocks": "0.
|
|
218
|
+
"@emdash-cms/blocks": "0.6.0"
|
|
214
219
|
},
|
|
215
220
|
"repository": {
|
|
216
221
|
"type": "git",
|
package/src/after.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defer work past the HTTP response.
|
|
3
|
+
*
|
|
4
|
+
* Use for bookkeeping that doesn't need to complete before the client
|
|
5
|
+
* gets bytes — writes that record state, maintenance queries, cache
|
|
6
|
+
* refreshes. `after()` hands the promise to the host's lifetime
|
|
7
|
+
* extender when one is available (Cloudflare's `waitUntil` under
|
|
8
|
+
* workerd), or fires-and-forgets on Node (the process lives for the
|
|
9
|
+
* next request anyway).
|
|
10
|
+
*
|
|
11
|
+
* Host binding is resolved lazily via a dynamic import of the
|
|
12
|
+
* `virtual:emdash/wait-until` virtual module. Lazy — rather than a
|
|
13
|
+
* static top-level import — so tools that walk the dist in a plain
|
|
14
|
+
* Node loader (`astro check`, Vitest, etc.) don't trip over the
|
|
15
|
+
* `virtual:` scheme: they'd only fail if they actually called
|
|
16
|
+
* `after()`, which they don't during type-checking.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type WaitUntilFn = (promise: Promise<unknown>) => void;
|
|
20
|
+
|
|
21
|
+
// Resolves to the host's waitUntil if the adapter provided one, or
|
|
22
|
+
// null otherwise. Kicked off once at module load; subsequent `after()`
|
|
23
|
+
// calls see the cached result without re-importing.
|
|
24
|
+
const waitUntilReady: Promise<WaitUntilFn | null> = (async () => {
|
|
25
|
+
try {
|
|
26
|
+
// @ts-ignore - virtual module, generated by the Astro integration
|
|
27
|
+
const mod = (await import("virtual:emdash/wait-until")) as {
|
|
28
|
+
waitUntil?: WaitUntilFn;
|
|
29
|
+
};
|
|
30
|
+
return mod.waitUntil ?? null;
|
|
31
|
+
} catch {
|
|
32
|
+
// No virtual module available (Node-side tooling, tests without the
|
|
33
|
+
// integration in scope). Fire-and-forget is the safe fallback.
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
// Surface rejections without making the module-load fail.
|
|
38
|
+
waitUntilReady.catch(() => {});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Schedule `fn` to run without blocking the response.
|
|
42
|
+
*
|
|
43
|
+
* Errors are caught and logged — a deferred task should never surface
|
|
44
|
+
* as an unhandled rejection because the response is long gone. Callers
|
|
45
|
+
* that care about errors should handle them inside `fn`.
|
|
46
|
+
*/
|
|
47
|
+
export function after(fn: () => void | Promise<void>): void {
|
|
48
|
+
const promise = Promise.resolve()
|
|
49
|
+
.then(fn)
|
|
50
|
+
.catch((error) => {
|
|
51
|
+
console.error("[emdash] deferred task failed:", error);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Defer the lifetime-extender handoff to the microtask that resolves
|
|
55
|
+
// waitUntilReady. On workerd this is effectively instant (the virtual
|
|
56
|
+
// module is already loaded in the bundle); on Node the promise
|
|
57
|
+
// resolves to null, so this is just one extra microtask and no-op.
|
|
58
|
+
void waitUntilReady.then((waitUntil) => {
|
|
59
|
+
if (waitUntil) waitUntil(promise);
|
|
60
|
+
return null;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
VALID_SCOPES,
|
|
21
21
|
} from "../../auth/api-tokens.js";
|
|
22
22
|
import type { Database } from "../../database/types.js";
|
|
23
|
+
import { validateRedirectUri } from "../oauth/redirect-uri.js";
|
|
23
24
|
import type { ApiResult } from "../types.js";
|
|
24
25
|
import { lookupOAuthClient, validateClientRedirectUri } from "./oauth-clients.js";
|
|
25
26
|
|
|
@@ -76,38 +77,7 @@ function expiresAt(seconds: number): string {
|
|
|
76
77
|
return new Date(Date.now() + seconds * 1000).toISOString();
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
* Validate a redirect URI per OAuth 2.1 security requirements.
|
|
81
|
-
* Allows localhost (loopback) over HTTP, and any HTTPS URL.
|
|
82
|
-
*/
|
|
83
|
-
export function validateRedirectUri(uri: string): string | null {
|
|
84
|
-
try {
|
|
85
|
-
const url = new URL(uri);
|
|
86
|
-
|
|
87
|
-
// Reject protocol-relative URLs
|
|
88
|
-
if (uri.startsWith("//")) {
|
|
89
|
-
return "Protocol-relative redirect URIs are not allowed";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Allow localhost/loopback over HTTP (for desktop MCP clients)
|
|
93
|
-
if (url.protocol === "http:") {
|
|
94
|
-
const host = url.hostname;
|
|
95
|
-
if (host === "127.0.0.1" || host === "localhost" || host === "[::1]") {
|
|
96
|
-
return null; // OK
|
|
97
|
-
}
|
|
98
|
-
return "HTTP redirect URIs are only allowed for localhost";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Allow HTTPS
|
|
102
|
-
if (url.protocol === "https:") {
|
|
103
|
-
return null; // OK
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return `Unsupported redirect URI scheme: ${url.protocol}`;
|
|
107
|
-
} catch {
|
|
108
|
-
return "Invalid redirect URI";
|
|
109
|
-
}
|
|
110
|
-
}
|
|
80
|
+
export { validateRedirectUri };
|
|
111
81
|
|
|
112
82
|
/**
|
|
113
83
|
* Validate and normalize scopes. Returns validated scope list.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { Kysely } from "kysely";
|
|
10
10
|
|
|
11
11
|
import type { Database } from "../../database/types.js";
|
|
12
|
+
import { validateRedirectUri } from "../oauth/redirect-uri.js";
|
|
12
13
|
import type { ApiResult } from "../types.js";
|
|
13
14
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
@@ -21,6 +22,16 @@ function parseJsonColumn<T>(value: string): T {
|
|
|
21
22
|
return JSON.parse(value) as T;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function validateRegisteredRedirectUris(redirectUris: string[]): string | null {
|
|
26
|
+
for (const redirectUri of redirectUris) {
|
|
27
|
+
const error = validateRedirectUri(redirectUri);
|
|
28
|
+
if (error) {
|
|
29
|
+
return `Invalid redirect URI: ${error}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
// ---------------------------------------------------------------------------
|
|
25
36
|
// Types
|
|
26
37
|
// ---------------------------------------------------------------------------
|
|
@@ -61,6 +72,17 @@ export async function handleOAuthClientCreate(
|
|
|
61
72
|
};
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
const redirectUriError = validateRegisteredRedirectUris(input.redirectUris);
|
|
76
|
+
if (redirectUriError) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: {
|
|
80
|
+
code: "VALIDATION_ERROR",
|
|
81
|
+
message: redirectUriError,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
// Check for duplicate client ID
|
|
65
87
|
const existing = await db
|
|
66
88
|
.selectFrom("_emdash_oauth_clients")
|
|
@@ -83,7 +105,7 @@ export async function handleOAuthClientCreate(
|
|
|
83
105
|
id: input.id,
|
|
84
106
|
name: input.name,
|
|
85
107
|
redirect_uris: JSON.stringify(input.redirectUris),
|
|
86
|
-
scopes: input.scopes ? JSON.stringify(input.scopes) : null,
|
|
108
|
+
scopes: input.scopes && input.scopes.length > 0 ? JSON.stringify(input.scopes) : null,
|
|
87
109
|
})
|
|
88
110
|
.execute();
|
|
89
111
|
|
|
@@ -93,7 +115,7 @@ export async function handleOAuthClientCreate(
|
|
|
93
115
|
id: input.id,
|
|
94
116
|
name: input.name,
|
|
95
117
|
redirectUris: input.redirectUris,
|
|
96
|
-
scopes: input.scopes
|
|
118
|
+
scopes: input.scopes && input.scopes.length > 0 ? input.scopes : null,
|
|
97
119
|
createdAt: now,
|
|
98
120
|
updatedAt: now,
|
|
99
121
|
},
|
|
@@ -222,7 +244,20 @@ export async function handleOAuthClientUpdate(
|
|
|
222
244
|
};
|
|
223
245
|
}
|
|
224
246
|
|
|
225
|
-
|
|
247
|
+
if (input.redirectUris !== undefined) {
|
|
248
|
+
const redirectUriError = validateRegisteredRedirectUris(input.redirectUris);
|
|
249
|
+
if (redirectUriError) {
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
error: {
|
|
253
|
+
code: "VALIDATION_ERROR",
|
|
254
|
+
message: redirectUriError,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const updates: Record<string, string | null> = {
|
|
226
261
|
updated_at: new Date().toISOString(),
|
|
227
262
|
};
|
|
228
263
|
|
|
@@ -233,7 +268,8 @@ export async function handleOAuthClientUpdate(
|
|
|
233
268
|
updates.redirect_uris = JSON.stringify(input.redirectUris);
|
|
234
269
|
}
|
|
235
270
|
if (input.scopes !== undefined) {
|
|
236
|
-
updates.scopes =
|
|
271
|
+
updates.scopes =
|
|
272
|
+
input.scopes && input.scopes.length > 0 ? JSON.stringify(input.scopes) : null;
|
|
237
273
|
}
|
|
238
274
|
|
|
239
275
|
await db.updateTable("_emdash_oauth_clients").set(updates).where("id", "=", clientId).execute();
|
|
@@ -7,6 +7,7 @@ import { ulid } from "ulidx";
|
|
|
7
7
|
|
|
8
8
|
import { TaxonomyRepository } from "../../database/repositories/taxonomy.js";
|
|
9
9
|
import type { Database } from "../../database/types.js";
|
|
10
|
+
import { invalidateTermCache } from "../../taxonomies/index.js";
|
|
10
11
|
import type { ApiResult } from "../types.js";
|
|
11
12
|
|
|
12
13
|
/** Taxonomy name validation pattern: lowercase alphanumeric + underscores, starts with letter */
|
|
@@ -323,6 +324,10 @@ export async function handleTermCreate(
|
|
|
323
324
|
data: input.description ? { description: input.description } : undefined,
|
|
324
325
|
});
|
|
325
326
|
|
|
327
|
+
// New term means `hasAnyTermAssignments` may flip from false->true next
|
|
328
|
+
// time an entry is tagged. Clear the cache so the next read re-probes.
|
|
329
|
+
invalidateTermCache();
|
|
330
|
+
|
|
326
331
|
return {
|
|
327
332
|
success: true,
|
|
328
333
|
data: {
|
|
@@ -442,6 +447,10 @@ export async function handleTermUpdate(
|
|
|
442
447
|
data: input.description !== undefined ? { description: input.description } : undefined,
|
|
443
448
|
});
|
|
444
449
|
|
|
450
|
+
// Term label/slug changes are reflected in hydrated entry.data.terms —
|
|
451
|
+
// invalidate so the next read doesn't short-circuit on a stale probe.
|
|
452
|
+
invalidateTermCache();
|
|
453
|
+
|
|
445
454
|
if (!updated) {
|
|
446
455
|
return {
|
|
447
456
|
success: false,
|
|
@@ -513,6 +522,10 @@ export async function handleTermDelete(
|
|
|
513
522
|
};
|
|
514
523
|
}
|
|
515
524
|
|
|
525
|
+
// Deleting a term cascades to content_taxonomies; invalidate so
|
|
526
|
+
// hydration no longer sees the stale assignments.
|
|
527
|
+
invalidateTermCache();
|
|
528
|
+
|
|
516
529
|
return { success: true, data: { deleted: true } };
|
|
517
530
|
} catch {
|
|
518
531
|
return {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a redirect URI per OAuth 2.1 security requirements.
|
|
3
|
+
*
|
|
4
|
+
* Allows localhost / loopback redirect URIs over HTTP for native clients,
|
|
5
|
+
* and any HTTPS URL for web-based flows.
|
|
6
|
+
*/
|
|
7
|
+
export function validateRedirectUri(uri: string): string | null {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(uri);
|
|
10
|
+
|
|
11
|
+
// Reject protocol-relative URLs
|
|
12
|
+
if (uri.startsWith("//")) {
|
|
13
|
+
return "Protocol-relative redirect URIs are not allowed";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Allow localhost/loopback over HTTP (for desktop MCP clients)
|
|
17
|
+
if (url.protocol === "http:") {
|
|
18
|
+
const host = url.hostname;
|
|
19
|
+
if (host === "127.0.0.1" || host === "localhost" || host === "[::1]") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return "HTTP redirect URIs are only allowed for localhost";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Allow HTTPS
|
|
26
|
+
if (url.protocol === "https:") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return `Unsupported redirect URI scheme: ${url.protocol}`;
|
|
31
|
+
} catch {
|
|
32
|
+
return "Invalid redirect URI";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
trashedContentListResponseSchema,
|
|
38
38
|
} from "../schemas/content.js";
|
|
39
39
|
import {
|
|
40
|
+
DEFAULT_MAX_UPLOAD_SIZE,
|
|
40
41
|
mediaConfirmBody,
|
|
41
42
|
mediaConfirmResponseSchema,
|
|
42
43
|
mediaExistingResponseSchema,
|
|
@@ -623,121 +624,123 @@ const contentPaths = {
|
|
|
623
624
|
// Media routes
|
|
624
625
|
// ---------------------------------------------------------------------------
|
|
625
626
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
627
|
+
function buildMediaPaths(maxUploadSize: number) {
|
|
628
|
+
return {
|
|
629
|
+
"/_emdash/api/media": {
|
|
630
|
+
get: {
|
|
631
|
+
operationId: "listMedia",
|
|
632
|
+
summary: "List media items",
|
|
633
|
+
tags: ["Media"],
|
|
634
|
+
requestParams: { query: mediaListQuery },
|
|
635
|
+
responses: {
|
|
636
|
+
"200": {
|
|
637
|
+
description: "Media list",
|
|
638
|
+
content: { [JSON_CONTENT]: { schema: successEnvelope(mediaListResponseSchema) } },
|
|
639
|
+
},
|
|
640
|
+
...authErrors,
|
|
641
|
+
...standardErrors(500),
|
|
637
642
|
},
|
|
638
|
-
...authErrors,
|
|
639
|
-
...standardErrors(500),
|
|
640
643
|
},
|
|
641
644
|
},
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
650
|
-
},
|
|
651
|
-
responses: {
|
|
652
|
-
"200": {
|
|
653
|
-
description: "Media item",
|
|
654
|
-
content: { [JSON_CONTENT]: { schema: successEnvelope(mediaResponseSchema) } },
|
|
645
|
+
"/_emdash/api/media/{id}": {
|
|
646
|
+
get: {
|
|
647
|
+
operationId: "getMedia",
|
|
648
|
+
summary: "Get a media item",
|
|
649
|
+
tags: ["Media"],
|
|
650
|
+
requestParams: {
|
|
651
|
+
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
655
652
|
},
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
description: "
|
|
671
|
-
|
|
653
|
+
responses: {
|
|
654
|
+
"200": {
|
|
655
|
+
description: "Media item",
|
|
656
|
+
content: { [JSON_CONTENT]: { schema: successEnvelope(mediaResponseSchema) } },
|
|
657
|
+
},
|
|
658
|
+
...authErrors,
|
|
659
|
+
...standardErrors(404, 500),
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
put: {
|
|
663
|
+
operationId: "updateMedia",
|
|
664
|
+
summary: "Update media metadata",
|
|
665
|
+
tags: ["Media"],
|
|
666
|
+
requestParams: {
|
|
667
|
+
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
668
|
+
},
|
|
669
|
+
requestBody: { content: { [JSON_CONTENT]: { schema: mediaUpdateBody } } },
|
|
670
|
+
responses: {
|
|
671
|
+
"200": {
|
|
672
|
+
description: "Updated media item",
|
|
673
|
+
content: { [JSON_CONTENT]: { schema: successEnvelope(mediaResponseSchema) } },
|
|
674
|
+
},
|
|
675
|
+
...authErrors,
|
|
676
|
+
...standardErrors(400, 404, 500),
|
|
672
677
|
},
|
|
673
|
-
...authErrors,
|
|
674
|
-
...standardErrors(400, 404, 500),
|
|
675
678
|
},
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
683
|
-
},
|
|
684
|
-
responses: {
|
|
685
|
-
"200": {
|
|
686
|
-
description: "Deleted",
|
|
687
|
-
content: { [JSON_CONTENT]: { schema: successEnvelope(deleteResponseSchema) } },
|
|
679
|
+
delete: {
|
|
680
|
+
operationId: "deleteMedia",
|
|
681
|
+
summary: "Delete a media item",
|
|
682
|
+
tags: ["Media"],
|
|
683
|
+
requestParams: {
|
|
684
|
+
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
688
685
|
},
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
686
|
+
responses: {
|
|
687
|
+
"200": {
|
|
688
|
+
description: "Deleted",
|
|
689
|
+
content: { [JSON_CONTENT]: { schema: successEnvelope(deleteResponseSchema) } },
|
|
690
|
+
},
|
|
691
|
+
...authErrors,
|
|
692
|
+
...standardErrors(404, 500),
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
"/_emdash/api/media/upload-url": {
|
|
697
|
+
post: {
|
|
698
|
+
operationId: "getMediaUploadUrl",
|
|
699
|
+
summary: "Get a signed URL for direct upload",
|
|
700
|
+
description:
|
|
701
|
+
"Returns a signed URL for direct-to-storage upload. Creates a pending media record.",
|
|
702
|
+
tags: ["Media"],
|
|
703
|
+
requestBody: { content: { [JSON_CONTENT]: { schema: mediaUploadUrlBody(maxUploadSize) } } },
|
|
704
|
+
responses: {
|
|
705
|
+
"200": {
|
|
706
|
+
description: "Upload URL or existing media (deduplication)",
|
|
707
|
+
content: {
|
|
708
|
+
[JSON_CONTENT]: {
|
|
709
|
+
schema: successEnvelope(
|
|
710
|
+
z.union([mediaUploadUrlResponseSchema, mediaExistingResponseSchema]),
|
|
711
|
+
),
|
|
712
|
+
},
|
|
710
713
|
},
|
|
711
714
|
},
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
},
|
|
716
|
-
},
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
715
|
+
...authErrors,
|
|
716
|
+
...standardErrors(400, 500),
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
"/_emdash/api/media/{id}/confirm": {
|
|
721
|
+
post: {
|
|
722
|
+
operationId: "confirmMediaUpload",
|
|
723
|
+
summary: "Confirm a media upload",
|
|
724
|
+
description: "Marks a pending media record as ready after the file has been uploaded.",
|
|
725
|
+
tags: ["Media"],
|
|
726
|
+
requestParams: {
|
|
727
|
+
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
728
|
+
},
|
|
729
|
+
requestBody: { content: { [JSON_CONTENT]: { schema: mediaConfirmBody } } },
|
|
730
|
+
responses: {
|
|
731
|
+
"200": {
|
|
732
|
+
description: "Confirmed media item with URL",
|
|
733
|
+
content: {
|
|
734
|
+
[JSON_CONTENT]: { schema: successEnvelope(mediaConfirmResponseSchema) },
|
|
735
|
+
},
|
|
733
736
|
},
|
|
737
|
+
...authErrors,
|
|
738
|
+
...standardErrors(400, 404, 500),
|
|
734
739
|
},
|
|
735
|
-
...authErrors,
|
|
736
|
-
...standardErrors(400, 404, 500),
|
|
737
740
|
},
|
|
738
741
|
},
|
|
739
|
-
}
|
|
740
|
-
}
|
|
742
|
+
} as const;
|
|
743
|
+
}
|
|
741
744
|
|
|
742
745
|
// ---------------------------------------------------------------------------
|
|
743
746
|
// Schema routes
|
|
@@ -2249,20 +2252,22 @@ const userPaths = {
|
|
|
2249
2252
|
// Merge all paths
|
|
2250
2253
|
// ---------------------------------------------------------------------------
|
|
2251
2254
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2255
|
+
function buildAllPaths(maxUploadSize: number) {
|
|
2256
|
+
return {
|
|
2257
|
+
...contentPaths,
|
|
2258
|
+
...buildMediaPaths(maxUploadSize),
|
|
2259
|
+
...schemaPaths,
|
|
2260
|
+
...commentsPaths,
|
|
2261
|
+
...taxonomyPaths,
|
|
2262
|
+
...menuPaths,
|
|
2263
|
+
...sectionPaths,
|
|
2264
|
+
...widgetPaths,
|
|
2265
|
+
...settingsPaths,
|
|
2266
|
+
...searchPaths,
|
|
2267
|
+
...redirectPaths,
|
|
2268
|
+
...userPaths,
|
|
2269
|
+
} as const;
|
|
2270
|
+
}
|
|
2266
2271
|
|
|
2267
2272
|
// ---------------------------------------------------------------------------
|
|
2268
2273
|
// Document
|
|
@@ -2274,7 +2279,10 @@ const allPaths = {
|
|
|
2274
2279
|
* Covers: Content, Media, Schema, Comments, Taxonomies, Menus,
|
|
2275
2280
|
* Sections, Widgets, Settings, Search, Redirects, Users.
|
|
2276
2281
|
*/
|
|
2277
|
-
export function generateOpenApiDocument(
|
|
2282
|
+
export function generateOpenApiDocument(
|
|
2283
|
+
options: { maxUploadSize?: number } = {},
|
|
2284
|
+
): oas31.OpenAPIObject {
|
|
2285
|
+
const maxUploadSize = options.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
|
|
2278
2286
|
return createDocument({
|
|
2279
2287
|
openapi: "3.1.0",
|
|
2280
2288
|
info: {
|
|
@@ -2363,6 +2371,6 @@ export function generateOpenApiDocument(): oas31.OpenAPIObject {
|
|
|
2363
2371
|
},
|
|
2364
2372
|
security: [{ session: [] }, { bearer: [] }],
|
|
2365
2373
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- readonly const paths are compatible at runtime
|
|
2366
|
-
paths:
|
|
2374
|
+
paths: buildAllPaths(maxUploadSize) as unknown as ZodOpenApiPathsObject,
|
|
2367
2375
|
});
|
|
2368
2376
|
}
|
package/src/api/schemas/auth.ts
CHANGED
|
@@ -60,6 +60,13 @@ export const inviteCreateBody = z
|
|
|
60
60
|
})
|
|
61
61
|
.meta({ id: "InviteCreateBody" });
|
|
62
62
|
|
|
63
|
+
export const inviteRegisterOptionsBody = z
|
|
64
|
+
.object({
|
|
65
|
+
token: z.string().min(1),
|
|
66
|
+
name: z.string().optional(),
|
|
67
|
+
})
|
|
68
|
+
.meta({ id: "InviteRegisterOptionsBody" });
|
|
69
|
+
|
|
63
70
|
export const inviteCompleteBody = z
|
|
64
71
|
.object({
|
|
65
72
|
token: z.string().min(1),
|