dsh-tiddlywiki 0.26.6 → 0.26.7
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/README.md +1 -0
- package/lib/client.js +27 -27
- package/lib/index.js +44 -3
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/endpoints.ts +27 -0
- package/src/client/note-widget.ts +8 -5
- package/src/client/status-cache.ts +7 -0
- package/src/client/tw-frame.ts +20 -4
- package/src/host/http.ts +34 -1
- package/src/host/routes.ts +16 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tiddlywiki",
|
|
3
3
|
"description": "TiddlyWiki 5 as the DSH persistent knowledge base: fifteen tiddlywiki_* agent tools (filtered search/recent/tags/batch/rewrite/trash/lint + git sync & conflict resolve), a full TiddlyWiki editor embedded in the GUI center column (same-origin proxy, works over Tailscale/LAN/domain), a right-side wiki tab, a per-session summary tab, a quick-note card with draft auto-save behind a single knowledge FAB, an optional clip bookmarklet that also saves selected images as attachments, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.26.
|
|
4
|
+
"version": "0.26.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"smoke:client": "node scripts/smoke-client.mjs",
|
|
87
87
|
"selftest": "node scripts/selftest.mjs",
|
|
88
88
|
"verify:static": "node scripts/verify-send-to-agent-bundle.mjs && node scripts/verify-render-bundle.mjs && node scripts/verify-wechat-publish-bundle.mjs && node scripts/verify-package-contents.mjs && node scripts/verify-version-consistency.mjs && node scripts/verify-constants.mjs",
|
|
89
|
-
"verify:unit": "node scripts/verify-clip-bridge.mjs && node scripts/verify-seed-error-policy.mjs && node scripts/verify-render-sanitizer.mjs && node scripts/verify-secret-masking.mjs && node scripts/verify-write-policy.mjs && node scripts/verify-workspace.mjs && node scripts/verify-restart-drain.mjs && node scripts/verify-language-pin.mjs && node scripts/verify-prompt.mjs && node scripts/verify-conflict-config-guards.mjs && node scripts/verify-config-patch.mjs && node scripts/verify-tool-views.mjs && npx tsx scripts/verify-plugin-runtime.mjs && node scripts/verify-wechat-adapters.mjs && node scripts/verify-wechat-docs-seed.mjs && node scripts/verify-wechat-publish.mjs && node scripts/verify-frame-guards.mjs && npx tsx scripts/verify-ready-policy.mjs && npx tsx scripts/verify-frame-surface.mjs && npx tsx scripts/verify-editor-popup.mjs && npx tsx scripts/verify-status-cache.mjs",
|
|
89
|
+
"verify:unit": "node scripts/verify-clip-bridge.mjs && node scripts/verify-seed-error-policy.mjs && node scripts/verify-render-sanitizer.mjs && node scripts/verify-secret-masking.mjs && node scripts/verify-write-policy.mjs && node scripts/verify-workspace.mjs && node scripts/verify-restart-drain.mjs && node scripts/verify-language-pin.mjs && node scripts/verify-prompt.mjs && node scripts/verify-conflict-config-guards.mjs && node scripts/verify-config-patch.mjs && node scripts/verify-tool-views.mjs && npx tsx scripts/verify-plugin-runtime.mjs && node scripts/verify-wechat-adapters.mjs && node scripts/verify-wechat-docs-seed.mjs && node scripts/verify-wechat-publish.mjs && npx tsx scripts/verify-tw-origin.mjs && node scripts/verify-frame-guards.mjs && npx tsx scripts/verify-ready-policy.mjs && npx tsx scripts/verify-frame-surface.mjs && npx tsx scripts/verify-editor-popup.mjs && npx tsx scripts/verify-status-cache.mjs",
|
|
90
90
|
"verify:e2e": "node scripts/verify-auth.mjs && node scripts/verify-anon-username.mjs && node scripts/verify-tools.mjs && node scripts/verify-audit-fixes.mjs && node scripts/verify-git-resolve.mjs && node scripts/verify-resilience.mjs && node scripts/verify-seed-send-to-agent.mjs && node scripts/verify-seeds-admin.mjs && node scripts/verify-wiki-switch.mjs",
|
|
91
91
|
"verify:large": "node scripts/verify-large-wiki.mjs",
|
|
92
92
|
"verify": "npm run verify:static && npm run verify:unit && npm run verify:e2e && npm run verify:large",
|
package/src/client/endpoints.ts
CHANGED
|
@@ -48,6 +48,33 @@ export const ADMIN_WIKI_LOCATION_ENDPOINT = `${ROUTE_PREFIX}/admin/wiki/location
|
|
|
48
48
|
export const ADMIN_WIKI_SWITCH_ENDPOINT = `${ROUTE_PREFIX}/admin/wiki/switch`
|
|
49
49
|
export const ADMIN_WIKI_RESET_ENDPOINT = `${ROUTE_PREFIX}/admin/wiki/reset`
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a TW proxy URL for THIS document (v0.26.7).
|
|
53
|
+
*
|
|
54
|
+
* On an http(s) page the relative proxy path is right — it keeps working behind
|
|
55
|
+
* whatever host/domain/HTTPS the user reached DSH on. But TiddlyWiki's own
|
|
56
|
+
* TiddlyWeb sync adaptor only loads when the DOCUMENT's protocol starts with
|
|
57
|
+
* `http` (`plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js`:
|
|
58
|
+
* `if($tw.browser && document.location.protocol.substr(0,4) === "http")`), and
|
|
59
|
+
* the DSH **desktop** app serves its renderer from the custom `dsh-app:`
|
|
60
|
+
* scheme: framed from there the wiki had no sync adaptor at all, so
|
|
61
|
+
* `$:/status/IsReadOnly` was never written, TW's read-only stylesheet (which
|
|
62
|
+
* treats a missing status as read-only) hid 添加条目 / 编辑 / 导入 … and no edit
|
|
63
|
+
* could be saved.
|
|
64
|
+
*
|
|
65
|
+
* `absolute` is the host's own loopback HTTP base (`twProxyAbsolute` /
|
|
66
|
+
* `twUrlAbsolute`, built from the port it actually listens on); it is used only
|
|
67
|
+
* when this page is NOT on http(s), i.e. exactly the desktop case, so an
|
|
68
|
+
* http(s) embedder is never sent to a different origin than its own.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveTwUrl(relative: string, absolute?: string): string {
|
|
71
|
+
const protocol = typeof location === 'undefined' ? '' : location.protocol
|
|
72
|
+
const origin = typeof location === 'undefined' ? undefined : location.origin
|
|
73
|
+
const notHttp = protocol !== 'http:' && protocol !== 'https:'
|
|
74
|
+
if (notHttp && typeof absolute === 'string' && absolute.length > 0) return absolute
|
|
75
|
+
return new URL(relative, origin).href
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
/** The subset of the `/sync` JSON body both client callers report on. */
|
|
52
79
|
export interface SyncResultPayload {
|
|
53
80
|
ok?: boolean
|
|
@@ -30,7 +30,7 @@ import { toast } from './toast.ts'
|
|
|
30
30
|
import { openEditorPopup, isEditorPopupOpen, isEditorPopupBlank } from './editor-popup.ts'
|
|
31
31
|
import { buildMarkdownEditor, type MarkdownEditor } from './markdown-editor.ts'
|
|
32
32
|
import { fetchStatus } from './status-cache.ts'
|
|
33
|
-
import { EDIT_ENDPOINT, GET_ENDPOINT, NOTE_ENDPOINT, RECENT_ENDPOINT, TAGS_ENDPOINT, UPLOAD_ENDPOINT } from './endpoints.ts'
|
|
33
|
+
import { EDIT_ENDPOINT, GET_ENDPOINT, NOTE_ENDPOINT, RECENT_ENDPOINT, TAGS_ENDPOINT, UPLOAD_ENDPOINT, resolveTwUrl } from './endpoints.ts'
|
|
34
34
|
|
|
35
35
|
const MAX_UPLOAD_BYTES = 64 * 1024 * 1024
|
|
36
36
|
|
|
@@ -557,7 +557,7 @@ export function createNoteWidget(): NoteWidgetHandle {
|
|
|
557
557
|
signal: AbortSignal.timeout(10_000),
|
|
558
558
|
})
|
|
559
559
|
const payload = (await res.json().catch(() => null)) as
|
|
560
|
-
| { ok?: boolean; title?: string; draftTitle?: string; twUrl?: string; error?: string }
|
|
560
|
+
| { ok?: boolean; title?: string; draftTitle?: string; twUrl?: string; twUrlAbsolute?: string; error?: string }
|
|
561
561
|
| null
|
|
562
562
|
if (!res.ok || payload?.ok !== true) {
|
|
563
563
|
toast(`打开失败:${payload?.error ?? `HTTP ${res.status}`}`)
|
|
@@ -575,9 +575,12 @@ export function createNoteWidget(): NoteWidgetHandle {
|
|
|
575
575
|
persistedSignature = draftSignature(title, text, tags)
|
|
576
576
|
loadedToken = null
|
|
577
577
|
// twUrl is the same-origin proxy path (e.g. /dsh-tiddlywiki/tw/);
|
|
578
|
-
//
|
|
579
|
-
// host/domain DSH is reached on (loopback,
|
|
580
|
-
|
|
578
|
+
// resolveTwUrl turns it into an absolute URL against this page's origin
|
|
579
|
+
// so the popup works from any host/domain DSH is reached on (loopback,
|
|
580
|
+
// LAN, Tailscale, HTTPS) — and, on the DSH desktop app (`dsh-app:`), to
|
|
581
|
+
// the host's loopback HTTP base, without which TW's sync adaptor refuses
|
|
582
|
+
// to load and the native editor would come up read-only.
|
|
583
|
+
const popupUrl = `${resolveTwUrl(payload.twUrl, payload.twUrlAbsolute)}#${encodeURIComponent(payload.draftTitle)}`
|
|
581
584
|
openEditorPopup(popupUrl, payload.title ?? title)
|
|
582
585
|
toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`)
|
|
583
586
|
return true
|
|
@@ -23,6 +23,13 @@ export interface StatusPayload {
|
|
|
23
23
|
url?: string
|
|
24
24
|
/** Same-origin TW proxy path (e.g. /dsh-tiddlywiki/tw/); the iframe base. */
|
|
25
25
|
twProxy?: string
|
|
26
|
+
/**
|
|
27
|
+
* Absolute twin of `twProxy` (host loopback HTTP base, v0.26.7). Used only
|
|
28
|
+
* when this document is not on http(s) — the DSH desktop app's `dsh-app:`
|
|
29
|
+
* renderer, where a relative path would leave TW without a sync adaptor.
|
|
30
|
+
* See `resolveTwUrl`.
|
|
31
|
+
*/
|
|
32
|
+
twProxyAbsolute?: string
|
|
26
33
|
wikiPath?: string
|
|
27
34
|
error?: string
|
|
28
35
|
git?: {
|
package/src/client/tw-frame.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* @module dsh-tiddlywiki/client/tw-frame
|
|
28
28
|
*/
|
|
29
29
|
import * as React from 'react'
|
|
30
|
-
import { RESTART_ENDPOINT } from './endpoints.ts'
|
|
30
|
+
import { RESTART_ENDPOINT, resolveTwUrl } from './endpoints.ts'
|
|
31
31
|
import { fetchStatus } from './status-cache.ts'
|
|
32
32
|
import { attachThemeSync, setThemeSyncConfig } from './theme-sync.ts'
|
|
33
33
|
|
|
@@ -234,8 +234,21 @@ export function createTwFrameSurface(skin: TwFrameSkin): TwFrameSurface {
|
|
|
234
234
|
const tryOnce = (attempt: number): void => {
|
|
235
235
|
if (disposed) return // unmounted: do not keep waiting or touch the frame
|
|
236
236
|
if (pendingHash !== hash) return // superseded by a newer request
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
// Cross-origin frame (v0.26.7): on the DSH desktop app the TW frame is
|
|
238
|
+
// loaded from the host's loopback HTTP origin — the only way TW's
|
|
239
|
+
// TiddlyWeb sync adaptor will load there — which makes it a different
|
|
240
|
+
// origin than the DSH page. Reading `$tw` inside it raises SecurityError,
|
|
241
|
+
// so we cannot watch for TW's boot: go straight to the full
|
|
242
|
+
// reload-with-hash fallback (TW honours the hash at startup).
|
|
243
|
+
let frameIsTw = false
|
|
244
|
+
try {
|
|
245
|
+
const frameTw = win as { $tw?: unknown }
|
|
246
|
+
frameIsTw = typeof frameTw.$tw === 'object' && frameTw.$tw !== null
|
|
247
|
+
} catch {
|
|
248
|
+
fallbackLoad(hash)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (!frameIsTw) {
|
|
239
252
|
if (attempt < 40) {
|
|
240
253
|
// Tracked so dispose() cancels the chain (v0.19.1 — the untracked
|
|
241
254
|
// 40×150ms retry kept the iframe/closure alive after unmount).
|
|
@@ -302,7 +315,10 @@ export function createTwFrameSurface(skin: TwFrameSkin): TwFrameSurface {
|
|
|
302
315
|
// matter which host/domain the user reached DSH on. Fall back to the
|
|
303
316
|
// legacy loopback `url` for older servers that do not send twProxy.
|
|
304
317
|
if (typeof payload.twProxy === 'string') {
|
|
305
|
-
|
|
318
|
+
// resolveTwUrl prefers the host's ABSOLUTE loopback base when THIS page
|
|
319
|
+
// is not on http(s) (the DSH desktop app's `dsh-app:` renderer), because
|
|
320
|
+
// TW refuses to load its sync adaptor anywhere else — see resolveTwUrl.
|
|
321
|
+
showFrame(resolveTwUrl(payload.twProxy, payload.twProxyAbsolute))
|
|
306
322
|
} else if (typeof payload.url === 'string') {
|
|
307
323
|
showFrame(payload.url)
|
|
308
324
|
} else {
|
package/src/host/http.ts
CHANGED
|
@@ -198,7 +198,40 @@ export function rejectCrossSiteWrite(
|
|
|
198
198
|
return true
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Read-only route guard: GET/HEAD only, no side effects on any other method. */
|
|
202
203
|
export function rejectNonRead(req: IncomingMessage, res: ServerResponse): boolean {
|
|
203
204
|
return rejectCrossSiteWrite(req, res, ['GET', 'HEAD'])
|
|
204
205
|
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Absolute loopback base for a same-origin proxy path, from a request's `Host`
|
|
209
|
+
* header (v0.26.7). Returns undefined when the header is unusable, in which
|
|
210
|
+
* case the caller simply omits the absolute twin and clients keep using the
|
|
211
|
+
* relative path.
|
|
212
|
+
*
|
|
213
|
+
* WHY THIS EXISTS: the DSH **desktop** app serves its renderer from the custom
|
|
214
|
+
* `dsh-app:` scheme, and TiddlyWiki's own TiddlyWeb sync adaptor refuses to load
|
|
215
|
+
* on a document whose protocol does not start with `http`
|
|
216
|
+
* (`plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js`:
|
|
217
|
+
* `if($tw.browser && document.location.protocol.substr(0,4) === "http")`).
|
|
218
|
+
* Embedded from `dsh-app://app/…` the wiki therefore had NO sync adaptor:
|
|
219
|
+
* `$:/status/IsReadOnly` was never written, TW's read-only stylesheet (which
|
|
220
|
+
* treats a MISSING status as read-only) hid 添加条目 / 日志 / 导入 / 管理器 /
|
|
221
|
+
* 编辑 …, and the saver fell back to local-only dirty tracking — an edit made
|
|
222
|
+
* there could not be saved either. The client falls back to this absolute URL
|
|
223
|
+
* only while its own document is not on http(s) (see `resolveTwUrl` in
|
|
224
|
+
* `src/client/endpoints.ts`), so http(s) embedders — loopback, LAN, Tailscale,
|
|
225
|
+
* domain, HTTPS — are never moved off their own origin.
|
|
226
|
+
*
|
|
227
|
+
* `http` is deliberate rather than `x-forwarded-proto`: the desktop app's host
|
|
228
|
+
* always listens on plain HTTP loopback, and only a renderer able to reach it
|
|
229
|
+
* ever reads this value.
|
|
230
|
+
*/
|
|
231
|
+
export function absoluteHostBase(hostHeader: string | string[] | undefined, proxyPath: string): string | undefined {
|
|
232
|
+
const host = typeof hostHeader === 'string' ? hostHeader.trim() : Array.isArray(hostHeader) ? String(hostHeader[0] ?? '').trim() : ''
|
|
233
|
+
// `host[:port]` only — a smuggled path or credentials must never reach the
|
|
234
|
+
// URL handed back to the browser.
|
|
235
|
+
if (!/^[A-Za-z0-9.\-[\]:]+$/.test(host)) return undefined
|
|
236
|
+
return `http://${host}${proxyPath}`
|
|
237
|
+
}
|
package/src/host/routes.ts
CHANGED
|
@@ -60,7 +60,7 @@ import { GitConflictStateError, type GitFace, type GitStatusView } from './git.t
|
|
|
60
60
|
import { PATH_PREFIX, TW_PROXY_PREFIX, TW_PROXY_PATH } from './wiki.ts'
|
|
61
61
|
import { writeSessionSummary, type SessionQueryFace, type SessionSummaryResult } from './session-summary.ts'
|
|
62
62
|
import { WORKSPACE_TAG_PREFIX } from './workspace.ts'
|
|
63
|
-
import { readBody, readBodyBuffer, json, guardHandler, errorStatus, rejectCrossSiteWrite, rejectNonRead, safeTokenEqual, MAX_PROXY_BODY_BYTES, MAX_UPLOAD_BYTES } from './http.ts'
|
|
63
|
+
import { readBody, readBodyBuffer, json, guardHandler, errorStatus, rejectCrossSiteWrite, rejectNonRead, safeTokenEqual, absoluteHostBase, MAX_PROXY_BODY_BYTES, MAX_UPLOAD_BYTES } from './http.ts'
|
|
64
64
|
import { sanitizeTwFragment } from './sanitize.ts'
|
|
65
65
|
import { drainThenStop } from './seeds.ts'
|
|
66
66
|
import { snippetOf, formatLocalMinute } from './text-util.ts'
|
|
@@ -628,6 +628,15 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
|
|
|
628
628
|
const referencesBlockedTitle = (value: string | undefined): boolean =>
|
|
629
629
|
value !== undefined && BLOCKED_PROXY_TITLE_PREFIXES.some((prefix) => value.includes(prefix))
|
|
630
630
|
|
|
631
|
+
/**
|
|
632
|
+
* Absolute twin of `twProxy` for embedders whose own document is not on
|
|
633
|
+
* http(s) — the DSH desktop app's `dsh-app:` renderer (v0.26.7). See
|
|
634
|
+
* `absoluteHostBase()` in http.ts for the full why; clients only consult it
|
|
635
|
+
* while their own page is not http(s), so nothing changes for http(s)
|
|
636
|
+
* deployments.
|
|
637
|
+
*/
|
|
638
|
+
const twProxyAbsoluteBase = (req: IncomingMessage): string | undefined => absoluteHostBase(req.headers.host, TW_PROXY_PATH)
|
|
639
|
+
|
|
631
640
|
const handleStatus = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
|
632
641
|
if (rejectNonRead(req, res)) return
|
|
633
642
|
const view = deps.server.status()
|
|
@@ -639,6 +648,10 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
|
|
|
639
648
|
// the spawn line's `password=…` (or a forwarded Authorization header).
|
|
640
649
|
logs: redactLogLines(view.logs),
|
|
641
650
|
twProxy: TW_PROXY_PATH,
|
|
651
|
+
// Absolute twin of `twProxy` for embedders whose own document is not on
|
|
652
|
+
// http(s) — the DSH desktop app (`dsh-app://app`); see
|
|
653
|
+
// twProxyAbsoluteBase(). Absent when the Host header is unusable.
|
|
654
|
+
twProxyAbsolute: twProxyAbsoluteBase(req),
|
|
642
655
|
git: gitSummary,
|
|
643
656
|
note: { tag: deps.noteDefaults().tag },
|
|
644
657
|
ui: deps.uiDefaults(),
|
|
@@ -707,7 +720,7 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
|
|
|
707
720
|
})
|
|
708
721
|
}
|
|
709
722
|
const result = await pendingSummary
|
|
710
|
-
json(res, { ok: true, ...result, twUrl: TW_PROXY_PATH })
|
|
723
|
+
json(res, { ok: true, ...result, twUrl: TW_PROXY_PATH, twUrlAbsolute: twProxyAbsoluteBase(req) })
|
|
711
724
|
} catch (err) {
|
|
712
725
|
json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, errorStatus(err))
|
|
713
726
|
}
|
|
@@ -1071,7 +1084,7 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
|
|
|
1071
1084
|
})
|
|
1072
1085
|
deps.autoCommit()
|
|
1073
1086
|
invalidateGitStatus()
|
|
1074
|
-
json(res, { ok: true, ...result, twUrl: TW_PROXY_PATH })
|
|
1087
|
+
json(res, { ok: true, ...result, twUrl: TW_PROXY_PATH, twUrlAbsolute: twProxyAbsoluteBase(req) })
|
|
1075
1088
|
} catch (err) {
|
|
1076
1089
|
if (err instanceof WriteConflictError) {
|
|
1077
1090
|
json(res, { ok: false, error: err.message, conflict: true }, 409)
|