@tita-agent/dsh-data-table 0.0.0-stage → 0.2.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.
Files changed (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -2
  3. package/cordis.patch.yml +7 -0
  4. package/lib/client.js +16766 -0
  5. package/lib/contract-envelope.d.ts +91 -0
  6. package/lib/contract-envelope.js +106 -0
  7. package/lib/contract-layout.d.ts +130 -0
  8. package/lib/contract-layout.js +138 -0
  9. package/lib/contract-refs.d.ts +39 -0
  10. package/lib/contract-refs.js +9 -0
  11. package/lib/contract-version.d.ts +311 -0
  12. package/lib/contract-version.js +270 -0
  13. package/lib/contract-wire.d.ts +147 -0
  14. package/lib/contract-wire.js +99 -0
  15. package/lib/contracts.d.ts +12 -0
  16. package/lib/contracts.js +12 -0
  17. package/lib/types/audit.d.ts +42 -0
  18. package/lib/types/audit.js +39 -0
  19. package/lib/types/client/ProjectImportMenu.d.ts +30 -0
  20. package/lib/types/client/ProjectImportMenu.js +70 -0
  21. package/lib/types/client/SheetPanel.d.ts +33 -0
  22. package/lib/types/client/SheetPanel.js +1766 -0
  23. package/lib/types/client/SheetToggle.d.ts +18 -0
  24. package/lib/types/client/SheetToggle.js +26 -0
  25. package/lib/types/client/index.d.ts +15 -0
  26. package/lib/types/client/index.js +110 -0
  27. package/lib/types/client/persistence/changes.d.ts +15 -0
  28. package/lib/types/client/persistence/changes.js +51 -0
  29. package/lib/types/client/persistence/ids.d.ts +34 -0
  30. package/lib/types/client/persistence/ids.js +48 -0
  31. package/lib/types/client/persistence/merge.d.ts +35 -0
  32. package/lib/types/client/persistence/merge.js +362 -0
  33. package/lib/types/client/store.d.ts +61 -0
  34. package/lib/types/client/store.js +152 -0
  35. package/lib/types/client/surface-context.d.ts +27 -0
  36. package/lib/types/client/surface-context.js +104 -0
  37. package/lib/types/client/surface-control.d.ts +2 -0
  38. package/lib/types/client/surface-control.js +12 -0
  39. package/lib/types/client/transport/workbookApi.d.ts +159 -0
  40. package/lib/types/client/transport/workbookApi.js +246 -0
  41. package/lib/types/client/univer/StandaloneWorkbookPage.d.ts +5 -0
  42. package/lib/types/client/univer/StandaloneWorkbookPage.js +7 -0
  43. package/lib/types/client/univer/WorkbookCanvas.d.ts +40 -0
  44. package/lib/types/client/univer/WorkbookCanvas.js +87 -0
  45. package/lib/types/client/univer/WorkbookView.d.ts +3 -0
  46. package/lib/types/client/univer/WorkbookView.js +2 -0
  47. package/lib/types/client/univer/loader.d.ts +18 -0
  48. package/lib/types/client/univer/loader.js +81 -0
  49. package/lib/types/client/univer/standalone-entry.d.ts +1 -0
  50. package/lib/types/client/univer/standalone-entry.js +19 -0
  51. package/lib/types/client/versions/VersionChrome.d.ts +33 -0
  52. package/lib/types/client/versions/VersionChrome.js +21 -0
  53. package/lib/types/client/versions/inspect.d.ts +70 -0
  54. package/lib/types/client/versions/inspect.js +88 -0
  55. package/lib/types/client/versions/preview-decoration.d.ts +11 -0
  56. package/lib/types/client/versions/preview-decoration.js +54 -0
  57. package/lib/types/client/versions/records.d.ts +19 -0
  58. package/lib/types/client/versions/records.js +70 -0
  59. package/lib/types/client/workspace-host.d.ts +61 -0
  60. package/lib/types/client/workspace-host.js +124 -0
  61. package/lib/types/collaboration-hub.d.ts +31 -0
  62. package/lib/types/collaboration-hub.js +69 -0
  63. package/lib/types/contract/ids.d.ts +24 -0
  64. package/lib/types/contract/ids.js +46 -0
  65. package/lib/types/contract/mutations.d.ts +25 -0
  66. package/lib/types/contract/mutations.js +782 -0
  67. package/lib/types/contract/operations.d.ts +54 -0
  68. package/lib/types/contract/operations.js +203 -0
  69. package/lib/types/contract/snapshots.d.ts +176 -0
  70. package/lib/types/contract/snapshots.js +351 -0
  71. package/lib/types/contract/univer-api-catalog.d.ts +29 -0
  72. package/lib/types/contract/univer-api-catalog.js +264 -0
  73. package/lib/types/errors.d.ts +8 -0
  74. package/lib/types/errors.js +12 -0
  75. package/lib/types/http/assets-routes.d.ts +3 -0
  76. package/lib/types/http/assets-routes.js +169 -0
  77. package/lib/types/http/collaboration.d.ts +10 -0
  78. package/lib/types/http/collaboration.js +154 -0
  79. package/lib/types/http/history.d.ts +15 -0
  80. package/lib/types/http/history.js +134 -0
  81. package/lib/types/http/project-files.d.ts +79 -0
  82. package/lib/types/http/project-files.js +206 -0
  83. package/lib/types/http/project-scope.d.ts +50 -0
  84. package/lib/types/http/project-scope.js +52 -0
  85. package/lib/types/http/routes.d.ts +98 -0
  86. package/lib/types/http/routes.js +323 -0
  87. package/lib/types/http/standalone.d.ts +9 -0
  88. package/lib/types/http/standalone.js +31 -0
  89. package/lib/types/http/trunk.d.ts +10 -0
  90. package/lib/types/http/trunk.js +322 -0
  91. package/lib/types/http/worktree.d.ts +9 -0
  92. package/lib/types/http/worktree.js +145 -0
  93. package/lib/types/index.d.ts +18 -0
  94. package/lib/types/index.js +156 -0
  95. package/lib/types/invariant.d.ts +15 -0
  96. package/lib/types/invariant.js +15 -0
  97. package/lib/types/request-params.d.ts +21 -0
  98. package/lib/types/request-params.js +76 -0
  99. package/lib/types/store/paths.d.ts +68 -0
  100. package/lib/types/store/paths.js +148 -0
  101. package/lib/types/store/project.d.ts +88 -0
  102. package/lib/types/store/project.js +120 -0
  103. package/lib/types/store/read.d.ts +110 -0
  104. package/lib/types/store/read.js +404 -0
  105. package/lib/types/store/retention.d.ts +27 -0
  106. package/lib/types/store/retention.js +111 -0
  107. package/lib/types/store/write.d.ts +22 -0
  108. package/lib/types/store/write.js +210 -0
  109. package/lib/types/tools/arguments.d.ts +40 -0
  110. package/lib/types/tools/arguments.js +130 -0
  111. package/lib/types/tools/cells.d.ts +50 -0
  112. package/lib/types/tools/cells.js +298 -0
  113. package/lib/types/tools/index.d.ts +2 -0
  114. package/lib/types/tools/index.js +250 -0
  115. package/lib/types/tools/render.d.ts +47 -0
  116. package/lib/types/tools/render.js +290 -0
  117. package/lib/types/tools/univer-api.d.ts +18 -0
  118. package/lib/types/tools/univer-api.js +198 -0
  119. package/lib/types/tools/workbook-catalog.d.ts +3 -0
  120. package/lib/types/tools/workbook-catalog.js +83 -0
  121. package/lib/types/tools/workbook-edit.d.ts +192 -0
  122. package/lib/types/tools/workbook-edit.js +912 -0
  123. package/lib/types/tools/workbook-find.d.ts +11 -0
  124. package/lib/types/tools/workbook-find.js +89 -0
  125. package/lib/types/tools/workbook-history.d.ts +2 -0
  126. package/lib/types/tools/workbook-history.js +59 -0
  127. package/lib/types/tools/workbook-read.d.ts +21 -0
  128. package/lib/types/tools/workbook-read.js +204 -0
  129. package/lib/types/types.d.ts +377 -0
  130. package/lib/types/types.js +34 -0
  131. package/lib/types/univer/apply-operation.d.ts +14 -0
  132. package/lib/types/univer/apply-operation.js +398 -0
  133. package/lib/types/univer/chunk.d.ts +68 -0
  134. package/lib/types/univer/chunk.js +16 -0
  135. package/lib/types/univer/decoration.d.ts +34 -0
  136. package/lib/types/univer/decoration.js +88 -0
  137. package/lib/types/univer/diagnostics.d.ts +31 -0
  138. package/lib/types/univer/diagnostics.js +141 -0
  139. package/lib/types/univer/engine.d.ts +33 -0
  140. package/lib/types/univer/engine.js +338 -0
  141. package/lib/types/univer/entry.d.ts +8 -0
  142. package/lib/types/univer/entry.js +11 -0
  143. package/lib/types/univer/facade.d.ts +107 -0
  144. package/lib/types/univer/facade.js +1 -0
  145. package/lib/types/univer/fonts.d.ts +202 -0
  146. package/lib/types/univer/fonts.js +140 -0
  147. package/lib/types/univer/plugins/collaboration.d.ts +23 -0
  148. package/lib/types/univer/plugins/collaboration.js +47 -0
  149. package/lib/types/univer/plugins/export.d.ts +39 -0
  150. package/lib/types/univer/plugins/export.js +246 -0
  151. package/lib/types/univer/plugins/history.d.ts +24 -0
  152. package/lib/types/univer/plugins/history.js +50 -0
  153. package/lib/types/univer/plugins/version.d.ts +13 -0
  154. package/lib/types/univer/plugins/version.js +13 -0
  155. package/lib/types/version/diff.d.ts +36 -0
  156. package/lib/types/version/diff.js +156 -0
  157. package/lib/types/version/grouping.d.ts +7 -0
  158. package/lib/types/version/grouping.js +94 -0
  159. package/lib/types/version/records.d.ts +17 -0
  160. package/lib/types/version/records.js +102 -0
  161. package/lib/types/version/session.d.ts +36 -0
  162. package/lib/types/version/session.js +89 -0
  163. package/lib/types/version/summary.d.ts +24 -0
  164. package/lib/types/version/summary.js +111 -0
  165. package/lib/types/workbook-manager.d.ts +191 -0
  166. package/lib/types/workbook-manager.js +935 -0
  167. package/lib/types/xlsx-export-worker.d.ts +5 -0
  168. package/lib/types/xlsx-export-worker.js +24 -0
  169. package/lib/types/xlsx-export.d.ts +21 -0
  170. package/lib/types/xlsx-export.js +371 -0
  171. package/lib/types/xlsx-import-worker.d.ts +1 -0
  172. package/lib/types/xlsx-import-worker.js +19 -0
  173. package/lib/univer-client.js +690 -0
  174. package/lib/workbook-page.js +23826 -0
  175. package/lib/xlsx-export-worker.js +104 -0
  176. package/lib/xlsx-import-worker.js +46 -0
  177. package/package.json +106 -3
  178. package/src/audit.ts +58 -0
  179. package/src/client/ProjectImportMenu.tsx +100 -0
  180. package/src/client/SheetPanel.module.css +1578 -0
  181. package/src/client/SheetPanel.tsx +1934 -0
  182. package/src/client/SheetToggle.module.css +73 -0
  183. package/src/client/SheetToggle.tsx +65 -0
  184. package/src/client/contract.d.ts +19 -0
  185. package/src/client/index.ts +131 -0
  186. package/src/client/persistence/changes.ts +58 -0
  187. package/src/client/persistence/ids.ts +60 -0
  188. package/src/client/persistence/merge.ts +392 -0
  189. package/src/client/store.ts +188 -0
  190. package/src/client/surface-context.ts +123 -0
  191. package/src/client/surface-control.ts +13 -0
  192. package/src/client/transport/workbookApi.ts +355 -0
  193. package/src/client/univer/SheetView.module.css +110 -0
  194. package/src/client/univer/StandaloneWorkbookPage.module.css +8 -0
  195. package/src/client/univer/StandaloneWorkbookPage.tsx +15 -0
  196. package/src/client/univer/WorkbookCanvas.tsx +128 -0
  197. package/src/client/univer/WorkbookView.module.css +30 -0
  198. package/src/client/univer/WorkbookView.tsx +3 -0
  199. package/src/client/univer/loader.ts +90 -0
  200. package/src/client/univer/react-dom-client.d.ts +10 -0
  201. package/src/client/univer/standalone-entry.tsx +19 -0
  202. package/src/client/univer/standalone.css +12 -0
  203. package/src/client/univer/univer-locales.d.ts +89 -0
  204. package/src/client/univer/univer-modules.d.ts +26 -0
  205. package/src/client/versions/VersionChrome.tsx +114 -0
  206. package/src/client/versions/inspect.ts +138 -0
  207. package/src/client/versions/preview-decoration.ts +54 -0
  208. package/src/client/versions/records.ts +78 -0
  209. package/src/client/workspace-host.ts +147 -0
  210. package/src/collaboration-hub.ts +83 -0
  211. package/src/contract/ids.ts +50 -0
  212. package/src/contract/mutations.ts +791 -0
  213. package/src/contract/operations.ts +221 -0
  214. package/src/contract/snapshots.ts +383 -0
  215. package/src/contract/univer-api-catalog.ts +289 -0
  216. package/src/css-modules.d.ts +6 -0
  217. package/src/errors.ts +25 -0
  218. package/src/http/assets-routes.ts +177 -0
  219. package/src/http/collaboration.ts +144 -0
  220. package/src/http/history.ts +151 -0
  221. package/src/http/project-files.ts +227 -0
  222. package/src/http/project-scope.ts +77 -0
  223. package/src/http/routes.ts +364 -0
  224. package/src/http/standalone.ts +32 -0
  225. package/src/http/trunk.ts +322 -0
  226. package/src/http/worktree.ts +119 -0
  227. package/src/index.ts +165 -0
  228. package/src/invariant.ts +17 -0
  229. package/src/request-params.ts +92 -0
  230. package/src/store/paths.ts +165 -0
  231. package/src/store/project.ts +137 -0
  232. package/src/store/read.ts +454 -0
  233. package/src/store/retention.ts +106 -0
  234. package/src/store/write.ts +230 -0
  235. package/src/tools/arguments.ts +128 -0
  236. package/src/tools/cells.ts +298 -0
  237. package/src/tools/index.ts +274 -0
  238. package/src/tools/render.ts +301 -0
  239. package/src/tools/univer-api.ts +243 -0
  240. package/src/tools/workbook-catalog.ts +94 -0
  241. package/src/tools/workbook-edit.ts +987 -0
  242. package/src/tools/workbook-find.ts +80 -0
  243. package/src/tools/workbook-history.ts +63 -0
  244. package/src/tools/workbook-read.ts +210 -0
  245. package/src/types.ts +432 -0
  246. package/src/univer/apply-operation.ts +369 -0
  247. package/src/univer/chunk.ts +77 -0
  248. package/src/univer/decoration.ts +111 -0
  249. package/src/univer/diagnostics.ts +136 -0
  250. package/src/univer/engine.ts +370 -0
  251. package/src/univer/entry.ts +14 -0
  252. package/src/univer/facade.ts +91 -0
  253. package/src/univer/fonts.ts +148 -0
  254. package/src/univer/overrides.css +33 -0
  255. package/src/univer/plugins/collaboration.ts +47 -0
  256. package/src/univer/plugins/export.ts +236 -0
  257. package/src/univer/plugins/history.ts +49 -0
  258. package/src/univer/plugins/version.ts +13 -0
  259. package/src/version/diff.ts +169 -0
  260. package/src/version/grouping.ts +96 -0
  261. package/src/version/records.ts +113 -0
  262. package/src/version/session.ts +90 -0
  263. package/src/version/summary.ts +117 -0
  264. package/src/workbook-manager.ts +1111 -0
  265. package/src/xlsx-export-worker.ts +34 -0
  266. package/src/xlsx-export.ts +373 -0
  267. package/src/xlsx-import-worker.ts +30 -0
@@ -0,0 +1,323 @@
1
+ import { DataSheetError } from "../errors.js";
2
+ import { WorkbookRevisionConflictError } from "../workbook-manager.js";
3
+ import { parseIndex } from "../request-params.js";
4
+ /**
5
+ * Describe a failed route, naming the path that failed.
6
+ *
7
+ * @param where - The path or prefix being served.
8
+ * @param error - Whatever was thrown.
9
+ * @returns a message a person can act on.
10
+ */
11
+ function described(where, error) {
12
+ return `${where} 处理失败:${error instanceof Error ? error.message : String(error)}`;
13
+ }
14
+ export function routeFor(routes, pathname) {
15
+ return routes.find(route => route.kind === 'exact' && route.path === pathname)
16
+ ?? routes
17
+ .filter(route => route.kind === 'prefix' && (pathname === route.path || pathname.startsWith(`${route.path}/`)))
18
+ .sort((left, right) => right.path.length - left.path.length)[0];
19
+ }
20
+ /** Adapt the HTTP handlers to the official Electron shell's Connection carrier. */
21
+ export async function dispatchConnectionRoute(routes, request, prefix) {
22
+ const carrierUrl = new URL(request.url);
23
+ const rawPath = carrierUrl.searchParams.get('path');
24
+ if (rawPath === null || !rawPath.startsWith(`${prefix}/`))
25
+ return new Response('not found', { status: 404 });
26
+ const target = new URL(rawPath, 'http://dsh.invalid');
27
+ const route = routeFor(routes, target.pathname);
28
+ if (route === undefined)
29
+ return new Response('not found', { status: 404 });
30
+ const body = request.body === null ? undefined : Buffer.from(await request.arrayBuffer());
31
+ let started = false;
32
+ let status = 200;
33
+ let headers = {};
34
+ let start;
35
+ const startedPromise = new Promise(resolve => { start = resolve; });
36
+ let streamController;
37
+ let closeListener;
38
+ const begin = (nextStatus = 200, nextHeaders) => {
39
+ if (started)
40
+ return;
41
+ started = true;
42
+ status = nextStatus;
43
+ headers = nextHeaders ?? {};
44
+ start();
45
+ };
46
+ const response = {
47
+ writeHead(nextStatus, nextHeaders = {}) { begin(nextStatus, nextHeaders); },
48
+ write(chunk) {
49
+ begin();
50
+ try {
51
+ streamController?.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ return (streamController?.desiredSize ?? 1) > 0;
57
+ },
58
+ end(chunk) {
59
+ begin();
60
+ try {
61
+ if (chunk !== undefined)
62
+ streamController?.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
63
+ streamController?.close();
64
+ }
65
+ catch { /* the browser cancelled the stream */ }
66
+ },
67
+ once(_event, listener) { closeListener = listener; },
68
+ on() { },
69
+ destroy() { closeListener?.(); try {
70
+ streamController?.close();
71
+ }
72
+ catch { /* already closed */ } },
73
+ };
74
+ const nodeHeaders = Object.fromEntries(request.headers);
75
+ nodeHeaders.host ??= '127.0.0.1';
76
+ const nodeRequest = {
77
+ method: request.method,
78
+ url: `${target.pathname}${target.search}`,
79
+ headers: nodeHeaders,
80
+ socket: { localAddress: '127.0.0.1' },
81
+ async *[Symbol.asyncIterator]() {
82
+ if (body !== undefined && body.byteLength > 0)
83
+ yield body;
84
+ },
85
+ };
86
+ const stream = new ReadableStream({
87
+ start(controller) {
88
+ streamController = controller;
89
+ void route.handler(nodeRequest, response).catch(error => {
90
+ if (!started)
91
+ begin(500, { 'content-type': 'application/json; charset=utf-8' });
92
+ response.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
93
+ });
94
+ },
95
+ cancel() { closeListener?.(); },
96
+ });
97
+ await startedPromise;
98
+ return new Response(stream, { status, headers });
99
+ }
100
+ export function registerConnectionRoutes(ctx, routes, prefix) {
101
+ const route = {
102
+ path: `/api/${prefix.slice(1).split('/', 1)[0]}`,
103
+ methods: ['GET', 'HEAD', 'POST'],
104
+ requestBody: 'buffered',
105
+ // A failed handler must say so. DSH's web server turns an exception into a bodyless 400,
106
+ // which is indistinguishable from a malformed request — the same blindness that made a
107
+ // missing declaration look like a broken canvas.
108
+ fetch: async (request) => {
109
+ try {
110
+ return await dispatchConnectionRoute(routes, request, prefix);
111
+ }
112
+ catch (error) {
113
+ return new Response(JSON.stringify({ error: described(prefix, error) }), {
114
+ status: 500,
115
+ headers: { 'content-type': 'application/json' },
116
+ });
117
+ }
118
+ },
119
+ };
120
+ ctx.effect(() => ctx.connection.fetch.register(route), `data-sheet: ${route.path} Connection route`);
121
+ const inject = ctx.inject;
122
+ if (typeof inject === 'function') {
123
+ inject.call(ctx, ['webServer'], webCtx => {
124
+ for (const legacyRoute of routes) {
125
+ // Same reason as above, on the other carrier: these are the `/data-sheet/...` URLs the
126
+ // browser console was reporting as bare 400s with nothing to go on.
127
+ const guarded = {
128
+ ...legacyRoute,
129
+ handler: async (req, res) => {
130
+ try {
131
+ await legacyRoute.handler(req, res);
132
+ }
133
+ catch (error) {
134
+ const message = described(legacyRoute.path, error);
135
+ console.warn(`[data-sheet] ${message}`);
136
+ if (!res.headersSent) {
137
+ res.writeHead(500, { 'content-type': 'application/json' });
138
+ res.end(JSON.stringify({ error: message }));
139
+ }
140
+ else {
141
+ res.end();
142
+ }
143
+ }
144
+ },
145
+ };
146
+ webCtx.effect(() => webCtx.webServer.register(guarded), `data-sheet: ${legacyRoute.path} web route`);
147
+ }
148
+ });
149
+ }
150
+ }
151
+ export async function readJsonBody(req) {
152
+ const chunks = [];
153
+ let size = 0;
154
+ for await (const chunk of req) {
155
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
156
+ size += buffer.length;
157
+ // Snapshot limits are 25 MiB; leave a small envelope margin while still
158
+ // allowing a legitimate checkpoint to reach the Host without a browser
159
+ // side `Failed to fetch` caused by an undersized request parser.
160
+ if (size > 32 * 1024 * 1024)
161
+ throw new DataSheetError('PAYLOAD_TOO_LARGE', 'request body too large', 413);
162
+ chunks.push(buffer);
163
+ }
164
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
165
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
166
+ throw new Error('body must be an object');
167
+ return parsed;
168
+ }
169
+ /**
170
+ * Keep plugin routes behind the same browser-origin fence as the Host API.
171
+ * Loopback is allowed by default; non-loopback authorities must be explicitly
172
+ * listed in DSH_TRUSTED_HOSTS or match the local listening address.
173
+ */
174
+ export function guardDataSheetRequest(req, res) {
175
+ const hostHeader = typeof req.headers.host === 'string' ? req.headers.host.trim().toLowerCase() : '';
176
+ if (hostHeader.length === 0) {
177
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
178
+ res.end(JSON.stringify({ ok: false, error: '缺少 Host' }));
179
+ return false;
180
+ }
181
+ let hostUrl;
182
+ try {
183
+ hostUrl = new URL(`http://${hostHeader}`);
184
+ }
185
+ catch {
186
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
187
+ res.end(JSON.stringify({ ok: false, error: 'Host 无效' }));
188
+ return false;
189
+ }
190
+ const hostname = hostUrl.hostname.toLowerCase();
191
+ const localAddress = (req.socket.localAddress ?? '').replace(/^::ffff:/, '').toLowerCase();
192
+ const loopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
193
+ || hostname === '[::1]';
194
+ const trusted = new Set((process.env.DSH_TRUSTED_HOSTS ?? '').split(',').map(item => item.trim().toLowerCase()).filter(Boolean));
195
+ const localMatch = localAddress.length > 0 && (hostname === localAddress || (localAddress === '::1' && hostname === 'localhost'));
196
+ if (!loopback && !localMatch && !trusted.has(hostHeader) && !trusted.has(hostname)) {
197
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
198
+ res.end(JSON.stringify({ ok: false, error: '请求 authority 不在信任范围内' }));
199
+ return false;
200
+ }
201
+ const fetchSite = req.headers['sec-fetch-site'];
202
+ if (fetchSite === 'cross-site') {
203
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
204
+ res.end(JSON.stringify({ ok: false, error: '拒绝跨站请求' }));
205
+ return false;
206
+ }
207
+ const origin = typeof req.headers.origin === 'string' ? req.headers.origin : undefined;
208
+ if (origin !== undefined) {
209
+ try {
210
+ if (new URL(origin).host.toLowerCase() !== hostUrl.host.toLowerCase())
211
+ throw new Error('origin mismatch');
212
+ }
213
+ catch {
214
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
215
+ res.end(JSON.stringify({ ok: false, error: 'Origin 不匹配' }));
216
+ return false;
217
+ }
218
+ }
219
+ return true;
220
+ }
221
+ /** Resolve the per-tab session used to scope isolated worktrees. EventSource
222
+ * cannot send custom headers, so collaboration clients may provide `session`
223
+ * as a query parameter; normal fetch clients use the header. */
224
+ export function requireDataSheetSession(req, res, url) {
225
+ const header = typeof req.headers['x-data-sheet-session'] === 'string' ? req.headers['x-data-sheet-session'] : '';
226
+ const query = url.searchParams.get('session') ?? '';
227
+ const value = (header || query).trim();
228
+ if (value.length === 0 || value.length > 256) {
229
+ writeOperationError(res, new DataSheetError('SESSION_SCOPE_DENIED', '缺少有效的 data-sheet session', 403));
230
+ return null;
231
+ }
232
+ return value;
233
+ }
234
+ export function writeOperationError(res, error) {
235
+ if (error instanceof WorkbookRevisionConflictError) {
236
+ res.writeHead(409, { 'content-type': 'application/json; charset=utf-8' });
237
+ res.end(JSON.stringify({
238
+ ok: false,
239
+ code: 'REVISION_CONFLICT',
240
+ error: error.message,
241
+ // Compatible legacy fields plus the unified cross-plugin body (PRD §5.1).
242
+ expectedRevision: error.expected,
243
+ revision: error.actual,
244
+ conflict: error.payload,
245
+ }));
246
+ return;
247
+ }
248
+ const dataSheetError = error instanceof DataSheetError ? error : null;
249
+ const status = dataSheetError?.status ?? 400;
250
+ const code = dataSheetError?.code ?? 'VALIDATION_ERROR';
251
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
252
+ res.end(JSON.stringify({ ok: false, code, error: error instanceof Error ? error.message : String(error), ...(dataSheetError?.details ?? {}) }));
253
+ }
254
+ /**
255
+ * The `?after=` revision cursor.
256
+ *
257
+ * An absent (or blank) cursor means "from the beginning", which is a real value,
258
+ * not an error. Anything else has to parse — this used to swallow every failure
259
+ * into `0`, so `?after=abc` did not fail, it silently replayed the *entire*
260
+ * operation log at the asking client. A malformed cursor is now a 400 like any
261
+ * other bad parameter, which is what the surrounding handlers already do.
262
+ */
263
+ export function parseRevision(value) {
264
+ const trimmed = value?.trim() ?? '';
265
+ if (trimmed.length === 0)
266
+ return 0;
267
+ // Decimal digits only. `Number()` would also accept `0x10` (16), `1e3` (1000)
268
+ // and `5.0` (5) — a cursor is a revision number, and accepting a spelling
269
+ // nobody writes is how a typo turns into a plausible-looking revision.
270
+ if (!/^\d+$/.test(trimmed))
271
+ throw new Error('after 必须是大于等于 0 的整数');
272
+ return parseIndex(Number(trimmed), 'after');
273
+ }
274
+ /**
275
+ * Matching for one workbook path.
276
+ *
277
+ * The id segment is `[^/]+` rather than an ASCII class: a workbook's id is now its
278
+ * file name (`yyyymmdd-中文名`, see `storageNameOf` in `#contracts`),
279
+ * and the browser percent-encodes it into the path. Spelling the character set out
280
+ * here would be a second copy of `storageSegment`'s rule — one that silently 404s a
281
+ * name the store happily created. What keeps a path inside the store is not this
282
+ * regex but the store itself: every path builder in `store/paths.ts` runs the id
283
+ * through `normalizeWorkbookId` before it becomes a file name.
284
+ */
285
+ const WORKBOOK_PATH = /^\/data-sheet\/workbook\/([^/]+?)(?:\.json)?(?:\/(.*))?$/;
286
+ const PLAIN_ROUTES = {
287
+ changes: 'changes', collaboration: 'collaboration', history: 'history',
288
+ 'history/diff': 'history-diff', range: 'range', presence: 'presence', operations: 'operation',
289
+ view: 'view', proposals: 'proposals', rollback: 'rollback', worktrees: 'worktrees',
290
+ };
291
+ /** Match one workbook path; null when nothing here claims it. */
292
+ export function matchWorkbookRoute(pathname) {
293
+ const match = WORKBOOK_PATH.exec(pathname);
294
+ if (match === null)
295
+ return null;
296
+ const id = match[1];
297
+ const rest = match[2];
298
+ if (rest === undefined)
299
+ return { kind: 'snapshot', id };
300
+ const plain = PLAIN_ROUTES[rest];
301
+ if (plain !== undefined)
302
+ return { kind: plain, id };
303
+ // Same `[^/]+` id segment as the workbook path above: worktree and proposal ids
304
+ // are minted from the workbook's, so they inherit its character set.
305
+ const proposalAction = /^proposals\/([^/]+)\/(commit|reject)$/.exec(rest);
306
+ if (proposalAction !== null) {
307
+ return { kind: 'proposal-action', id, proposalId: proposalAction[1], action: proposalAction[2] };
308
+ }
309
+ const worktreeAction = /^worktrees\/([^/]+)\/(ready|reopen|merge|discard)$/.exec(rest);
310
+ if (worktreeAction !== null) {
311
+ return { kind: 'worktree-action', id, worktreeId: worktreeAction[1], action: worktreeAction[2] };
312
+ }
313
+ const worktreeChild = /^worktrees\/([^/]+)(?:\/(operations|diff))?$/.exec(rest);
314
+ if (worktreeChild !== null) {
315
+ const worktreeId = worktreeChild[1];
316
+ if (worktreeChild[2] === 'operations')
317
+ return { kind: 'worktree-operations', id, worktreeId };
318
+ if (worktreeChild[2] === 'diff')
319
+ return { kind: 'worktree-diff', id, worktreeId };
320
+ return { kind: 'worktree', id, worktreeId };
321
+ }
322
+ return null;
323
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The standalone workbook viewer (docs/issue/0022 P3).
3
+ *
4
+ * One workbook, no shell: the panel can hand a workbook to a plain page, which
5
+ * is why this HTML is assembled on the server and served as `…/view`.
6
+ */
7
+ export declare function escapeHtml(value: string): string;
8
+ /** Minimal same-origin shell; the React page bundle owns the workbook UI. */
9
+ export declare function standaloneWorkbookHtml(title: string): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The standalone workbook viewer (docs/issue/0022 P3).
3
+ *
4
+ * One workbook, no shell: the panel can hand a workbook to a plain page, which
5
+ * is why this HTML is assembled on the server and served as `…/view`.
6
+ */
7
+ export function escapeHtml(value) {
8
+ return value.replace(/[&<>'"]/g, character => ({
9
+ '&': '&amp;',
10
+ '<': '&lt;',
11
+ '>': '&gt;',
12
+ "'": '&#39;',
13
+ '"': '&quot;',
14
+ })[character] ?? character);
15
+ }
16
+ /** Minimal same-origin shell; the React page bundle owns the workbook UI. */
17
+ export function standaloneWorkbookHtml(title) {
18
+ const safeTitle = escapeHtml(title);
19
+ return `<!doctype html>
20
+ <html lang="zh-CN">
21
+ <head>
22
+ <meta charset="utf-8">
23
+ <meta name="viewport" content="width=device-width, initial-scale=1">
24
+ <title>${safeTitle} - Table</title>
25
+ </head>
26
+ <body>
27
+ <div id="data-sheet-workbook-root"></div>
28
+ <script src="/data-sheet/workbook-page.js"></script>
29
+ </body>
30
+ </html>`;
31
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The workbook's own routes (docs/issue/0022 P3).
3
+ *
4
+ * Snapshot read/write, the incremental `changes` cursor, one bounded range read,
5
+ * the operation append, proposals and rollback — everything a browser or another
6
+ * plugin uses to read and write the trunk workbook.
7
+ */
8
+ import type { IncomingMessage, ServerResponse } from 'node:http';
9
+ import { type WorkbookRoute } from './routes.ts';
10
+ export declare function handleTrunkRoute(route: WorkbookRoute, req: IncomingMessage, res: ServerResponse, url: URL): Promise<boolean>;