@civilization/dsh-drawio 0.1.0 → 0.2.1

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/src/index.js CHANGED
@@ -6,9 +6,11 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
6
6
  import { editDrawio, inspectDrawio, normalizeDrawio } from './xml.js'
7
7
 
8
8
  export const name = 'dsh-drawio'
9
- export const inject = ['tools', 'fs', 'sandbox', 'webServer']
9
+ export const inject = ['tools', 'fs', 'webServer']
10
10
 
11
11
  const runtimeRoot = fileURLToPath(new URL('../vendor/drawio/', import.meta.url))
12
+ const MAX_DIAGRAM_BYTES = 10 * 1024 * 1024
13
+ const MAX_REQUEST_BYTES = MAX_DIAGRAM_BYTES + 64 * 1024
12
14
  const output = {
13
15
  schema: { type: 'object', additionalProperties: true, properties: {} },
14
16
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
@@ -17,6 +19,7 @@ const pathParameter = { type: 'string', required: true, description: 'Path to a
17
19
 
18
20
  export function apply(ctx) {
19
21
  ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/dsh-drawio/runtime', handler: serveRuntime }))
22
+ ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/dsh-drawio/api', handler: (req, res) => serveCanvasApi(ctx, req, res) }))
20
23
  register(ctx, defineTool({
21
24
  name: 'drawio_inspect',
22
25
  description: 'Read a Draw.io file as structured pages, nodes, edges and geometry. Use this before editing an existing diagram. Set include_xml only when raw XML details are required.',
@@ -51,7 +54,8 @@ export function apply(ctx) {
51
54
  async execute(args, exec) {
52
55
  const { text, target } = await readDiagram(ctx, args.path, exec)
53
56
  const xml = editDrawio(text, args.operations, args.page)
54
- const outcome = await writeDiagram(ctx, target, xml, args, exec)
57
+ const policy = await resolvePolicy(ctx, 'drawio_edit', args, exec)
58
+ const outcome = await writeDiagram(ctx, target, xml, args, exec, policy)
55
59
  return { path: target.displayPath, operation: outcome.operation, operationsApplied: args.operations.length, ...inspectDrawio(xml, false) }
56
60
  },
57
61
  }))
@@ -62,7 +66,7 @@ export function apply(ctx) {
62
66
  output,
63
67
  async execute(args, exec) {
64
68
  assertDrawioPath(args.path)
65
- const policy = await ctx.sandbox.resolvePolicy('write', args, exec)
69
+ const policy = await resolvePolicy(ctx, 'drawio_write', args, exec)
66
70
  const target = await ctx.fs.resolve(args.path, resolveOptions(exec, args.path, policy?.workspaceRoot))
67
71
  const xml = normalizeDrawio(args.xml)
68
72
  const outcome = await writeDiagram(ctx, target, xml, args, exec, policy)
@@ -71,6 +75,67 @@ export function apply(ctx) {
71
75
  }))
72
76
  }
73
77
 
78
+ async function serveCanvasApi(ctx, req, res) {
79
+ if (req.method === 'OPTIONS') {
80
+ res.statusCode = 204
81
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
82
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
83
+ res.end()
84
+ return
85
+ }
86
+ if (req.method !== 'POST') return respondJson(res, 405, { ok: false, error: { message: 'Method Not Allowed' } })
87
+ try {
88
+ const pathname = new URL(req.url || '/', 'http://dsh.internal').pathname
89
+ const method = pathname.slice('/dsh-drawio/api/'.length)
90
+ if (!['read', 'write'].includes(method)) return respondJson(res, 404, { ok: false, error: { message: 'Unknown Draw.io operation.' } })
91
+ const payload = await readJsonBody(req)
92
+ const target = await resolveCanvasTarget(ctx, payload.cwd, payload.path)
93
+ const info = await ctx.fs.stat(target)
94
+ if (!info || info.type !== 'file') throw new Error('Draw.io file was not found.')
95
+ if (Number(info.size) > MAX_DIAGRAM_BYTES) throw new Error('Draw.io file is too large to open safely.')
96
+ if (method === 'read') return respondJson(res, 200, { ok: true, value: { content: await ctx.fs.readText(target) } })
97
+ if (typeof payload.content !== 'string' || Buffer.byteLength(payload.content) > MAX_DIAGRAM_BYTES) throw new Error('Draw.io content is too large to save safely.')
98
+ const xml = normalizeDrawio(payload.content)
99
+ const sandboxPolicy = { mode: 'workspace-write', workspaceRoot: payload.cwd }
100
+ await ctx.fs.writeText(target, xml, { kind: 'replaceIfVersion', version: info.version }, undefined, sandboxPolicy)
101
+ return respondJson(res, 200, { ok: true, value: { ok: true } })
102
+ } catch (error) {
103
+ return respondJson(res, 400, { ok: false, error: { message: String(error?.message || error).slice(0, 500) } })
104
+ }
105
+ }
106
+
107
+ async function resolveCanvasTarget(ctx, cwd, path) {
108
+ if (typeof cwd !== 'string' || !cwd.trim()) throw new Error('A workspace is required.')
109
+ assertDrawioPath(path)
110
+ const [root, target] = await Promise.all([ctx.fs.resolve(cwd), ctx.fs.resolve(path, { cwd })])
111
+ if (!ctx.fs.contains(root, target)) throw new Error('Draw.io file must stay inside the current workspace.')
112
+ return target
113
+ }
114
+
115
+ async function readJsonBody(req) {
116
+ const declared = Number(req.headers?.['content-length'])
117
+ if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES) throw new Error('Request body is too large.')
118
+ const chunks = []; let size = 0
119
+ for await (const chunk of req) {
120
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
121
+ size += buffer.length
122
+ if (size > MAX_REQUEST_BYTES) throw new Error('Request body is too large.')
123
+ chunks.push(buffer)
124
+ }
125
+ const text = Buffer.concat(chunks).toString('utf8')
126
+ const value = text ? JSON.parse(text) : {}
127
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Request body must be a JSON object.')
128
+ return value
129
+ }
130
+
131
+ function respondJson(res, status, value) {
132
+ const body = JSON.stringify(value)
133
+ res.statusCode = status
134
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
135
+ res.setHeader('Content-Length', Buffer.byteLength(body))
136
+ res.end(body)
137
+ }
138
+
74
139
  function register(ctx, tool) {
75
140
  ctx.effect(() => ctx.tools.register(tool))
76
141
  }
@@ -86,8 +151,24 @@ async function readDiagram(ctx, requestedPath, exec) {
86
151
  return { target, text }
87
152
  }
88
153
 
154
+ async function resolvePolicy(ctx, toolName, args, exec) {
155
+ const sandbox = ctx.get?.('sandbox')
156
+ if (sandbox?.resolvePolicy) {
157
+ try {
158
+ return await sandbox.resolvePolicy(toolName, args, exec)
159
+ } catch {}
160
+ }
161
+ const policyService = ctx.get?.('sandboxPolicy')
162
+ if (policyService?.resolve) {
163
+ try {
164
+ return policyService.resolve({ ...(exec?.agent ? { session: exec.agent.session } : {}) })
165
+ } catch {}
166
+ }
167
+ return undefined
168
+ }
169
+
89
170
  async function writeDiagram(ctx, target, xml, args, exec, resolvedPolicy) {
90
- const policy = resolvedPolicy ?? await ctx.sandbox.resolvePolicy('write', args, exec)
171
+ const policy = resolvedPolicy ?? await resolvePolicy(ctx, 'write', args, exec)
91
172
  const intent = await ctx.waterfall('fs/write-intent', target, exec, () => undefined)
92
173
  const outcome = await ctx.fs.writeText(target, xml, intent, exec.signal, policy)
93
174
  ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec)
@@ -8,10 +8,16 @@ window.DRAW_MATH_URL = null;
8
8
  window.DRAWIO_CONFIG = {
9
9
  compressXml: false,
10
10
  enableCssDarkMode: true,
11
- defaultLibraries: 'general;flowchart;basic;arrows2',
12
- enabledLibraries: ['general', 'flowchart', 'basic', 'arrows2']
11
+ defaultLibraries: 'general;uml;er;bpmn;flowchart;basic;arrows2',
12
+ enabledLibraries: null
13
13
  };
14
14
  urlParams['sync'] = 'manual';
15
15
  urlParams['offline'] = '1';
16
16
  urlParams['local'] = '1';
17
17
  urlParams['plugins'] = '0';
18
+
19
+ if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
20
+ navigator.serviceWorker.getRegistrations().then(function(regs) {
21
+ for (var i = 0; i < regs.length; i++) regs[i].unregister();
22
+ }).catch(function() {});
23
+ }