@craft-native/craft 0.0.19 → 0.0.23

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 (2) hide show
  1. package/dist/index.js +88 -46
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1085,6 +1085,14 @@ class Timer {
1085
1085
  }
1086
1086
  // src/bridge/core.ts
1087
1087
  var import_events = require("events");
1088
+ var BridgeErrorCodes = {
1089
+ UNKNOWN: -1,
1090
+ TIMEOUT: -2,
1091
+ QUEUE_FULL: -3,
1092
+ BINARY_DISABLED: -4,
1093
+ EXPECTED_BINARY: -5,
1094
+ BRIDGE_DESTROYED: -6
1095
+ };
1088
1096
  var messageIdCounter = 0;
1089
1097
  function generateMessageId() {
1090
1098
  return `msg_${Date.now()}_${++messageIdCounter}`;
@@ -1123,7 +1131,8 @@ class NativeBridge extends import_events.EventEmitter {
1123
1131
  let data;
1124
1132
  try {
1125
1133
  data = "detail" in event ? event.detail : typeof event.data === "string" ? JSON.parse(event.data) : event.data;
1126
- } catch {
1134
+ } catch (err) {
1135
+ console.warn("[Craft Bridge] Failed to parse message:", err);
1127
1136
  return;
1128
1137
  }
1129
1138
  if (!data || !data.id)
@@ -1177,27 +1186,28 @@ class NativeBridge extends import_events.EventEmitter {
1177
1186
  break;
1178
1187
  }
1179
1188
  }
1180
- async request(method, params) {
1181
- return this.requestWithRetry(method, params, this.config.retries);
1189
+ async request(method, params, options) {
1190
+ return this.requestWithRetry(method, params, this.config.retries, options?.timeout);
1182
1191
  }
1183
- async requestWithRetry(method, params, retriesLeft) {
1192
+ async requestWithRetry(method, params, retriesLeft, timeout) {
1184
1193
  try {
1185
- return await this.sendRequest(method, params);
1194
+ return await this.sendRequest(method, params, timeout);
1186
1195
  } catch (error) {
1187
1196
  if (retriesLeft > 0 && this.isRetryableError(error)) {
1188
1197
  await this.delay(this.config.retryDelay);
1189
- return this.requestWithRetry(method, params, retriesLeft - 1);
1198
+ return this.requestWithRetry(method, params, retriesLeft - 1, timeout);
1190
1199
  }
1191
1200
  throw error;
1192
1201
  }
1193
1202
  }
1194
1203
  isRetryableError(error) {
1195
1204
  if (error instanceof BridgeError) {
1196
- return error.code === -1 || error.code === -2;
1205
+ return error.code === BridgeErrorCodes.UNKNOWN || error.code === BridgeErrorCodes.TIMEOUT;
1197
1206
  }
1198
1207
  return false;
1199
1208
  }
1200
- async sendRequest(method, params) {
1209
+ async sendRequest(method, params, timeout) {
1210
+ const effectiveTimeout = timeout ?? this.config.timeout;
1201
1211
  const message = {
1202
1212
  id: generateMessageId(),
1203
1213
  type: "request",
@@ -1206,7 +1216,7 @@ class NativeBridge extends import_events.EventEmitter {
1206
1216
  };
1207
1217
  if (!this.connected && this.config.enableOfflineQueue) {
1208
1218
  if (this.offlineQueue.length >= this.config.queueSize) {
1209
- throw new BridgeError("Offline queue full", -3);
1219
+ throw new BridgeError("Offline queue full", BridgeErrorCodes.QUEUE_FULL);
1210
1220
  }
1211
1221
  this.offlineQueue.push(message);
1212
1222
  return new Promise((resolve, reject) => {
@@ -1215,20 +1225,20 @@ class NativeBridge extends import_events.EventEmitter {
1215
1225
  reject,
1216
1226
  timeout: setTimeout(() => {
1217
1227
  this.pendingRequests.delete(message.id);
1218
- reject(new BridgeError("Request timeout", -1));
1219
- }, this.config.timeout)
1228
+ reject(new BridgeError("Request timeout", BridgeErrorCodes.UNKNOWN));
1229
+ }, effectiveTimeout)
1220
1230
  });
1221
1231
  });
1222
1232
  }
1223
1233
  return new Promise((resolve, reject) => {
1224
- const timeout = setTimeout(() => {
1234
+ const timer2 = setTimeout(() => {
1225
1235
  this.pendingRequests.delete(message.id);
1226
- reject(new BridgeError("Request timeout", -1));
1227
- }, this.config.timeout);
1236
+ reject(new BridgeError("Request timeout", BridgeErrorCodes.UNKNOWN));
1237
+ }, effectiveTimeout);
1228
1238
  this.pendingRequests.set(message.id, {
1229
1239
  resolve,
1230
1240
  reject,
1231
- timeout
1241
+ timeout: timer2
1232
1242
  });
1233
1243
  this.send(message);
1234
1244
  });
@@ -1284,20 +1294,20 @@ class NativeBridge extends import_events.EventEmitter {
1284
1294
  }
1285
1295
  async sendBinary(method, data) {
1286
1296
  if (!this.config.enableBinaryTransfer) {
1287
- throw new BridgeError("Binary transfer not enabled", -4);
1297
+ throw new BridgeError("Binary transfer not enabled", BridgeErrorCodes.BINARY_DISABLED);
1288
1298
  }
1289
1299
  const base64 = this.arrayBufferToBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data);
1290
1300
  await this.request(method, { _binary: true, data: base64 });
1291
1301
  }
1292
1302
  async receiveBinary(method, params) {
1293
1303
  if (!this.config.enableBinaryTransfer) {
1294
- throw new BridgeError("Binary transfer not enabled", -4);
1304
+ throw new BridgeError("Binary transfer not enabled", BridgeErrorCodes.BINARY_DISABLED);
1295
1305
  }
1296
1306
  const result = await this.request(method, params);
1297
1307
  if (result._binary) {
1298
1308
  return this.base64ToArrayBuffer(result.data);
1299
1309
  }
1300
- throw new BridgeError("Expected binary response", -5);
1310
+ throw new BridgeError("Expected binary response", BridgeErrorCodes.EXPECTED_BINARY);
1301
1311
  }
1302
1312
  async batch(requests) {
1303
1313
  return this.request("_batch", { requests });
@@ -1376,11 +1386,12 @@ class NativeBridge extends import_events.EventEmitter {
1376
1386
  return new Promise((resolve) => setTimeout(resolve, ms));
1377
1387
  }
1378
1388
  arrayBufferToBase64(buffer) {
1379
- let binary = "";
1380
- for (let i = 0;i < buffer.byteLength; i++) {
1381
- binary += String.fromCharCode(buffer[i]);
1389
+ const chunks = [];
1390
+ const chunkSize = 8192;
1391
+ for (let i = 0;i < buffer.byteLength; i += chunkSize) {
1392
+ chunks.push(String.fromCharCode(...buffer.subarray(i, i + chunkSize)));
1382
1393
  }
1383
- return btoa(binary);
1394
+ return btoa(chunks.join(""));
1384
1395
  }
1385
1396
  base64ToArrayBuffer(base64) {
1386
1397
  const binary = atob(base64);
@@ -1394,7 +1405,7 @@ class NativeBridge extends import_events.EventEmitter {
1394
1405
  destroy() {
1395
1406
  for (const [id, pending] of this.pendingRequests) {
1396
1407
  clearTimeout(pending.timeout);
1397
- pending.reject(new BridgeError("Bridge destroyed", -6));
1408
+ pending.reject(new BridgeError("Bridge destroyed", BridgeErrorCodes.BRIDGE_DESTROYED));
1398
1409
  }
1399
1410
  this.pendingRequests.clear();
1400
1411
  this.streams.clear();
@@ -2345,8 +2356,18 @@ var notify = (options) => appManager.notify(options);
2345
2356
  var registerShortcut = (accelerator, handler) => appManager.registerShortcut(accelerator, handler);
2346
2357
  var unregisterShortcut = (accelerator) => appManager.unregisterShortcut(accelerator);
2347
2358
  // src/api/fs.ts
2359
+ function validatePath(path) {
2360
+ const normalized = path.replace(/\\/g, "/");
2361
+ if (normalized.includes("/../") || normalized.startsWith("../") || normalized.endsWith("/..") || normalized === "..") {
2362
+ throw new Error(`Path traversal detected: "${path}" contains ".." segments`);
2363
+ }
2364
+ if (path.includes("\x00")) {
2365
+ throw new Error(`Invalid path: contains null byte`);
2366
+ }
2367
+ }
2348
2368
  var fs = {
2349
2369
  async readFile(path) {
2370
+ validatePath(path);
2350
2371
  if (typeof window !== "undefined" && window.craft?.fs) {
2351
2372
  return window.craft.fs.readFile(path);
2352
2373
  }
@@ -2354,6 +2375,7 @@ var fs = {
2354
2375
  return readFile(path, "utf-8");
2355
2376
  },
2356
2377
  async writeFile(path, content) {
2378
+ validatePath(path);
2357
2379
  if (typeof window !== "undefined" && window.craft?.fs) {
2358
2380
  return window.craft.fs.writeFile(path, content);
2359
2381
  }
@@ -2395,6 +2417,7 @@ var fs = {
2395
2417
  }
2396
2418
  };
2397
2419
  async function readBinaryFile(path) {
2420
+ validatePath(path);
2398
2421
  if (typeof window !== "undefined" && window.craft) {
2399
2422
  const response = await window.craft.bridge?.call("fs.readBinaryFile", { path });
2400
2423
  return new Uint8Array(response.data);
@@ -2403,6 +2426,7 @@ async function readBinaryFile(path) {
2403
2426
  return readFile(path);
2404
2427
  }
2405
2428
  async function writeBinaryFile(path, data) {
2429
+ validatePath(path);
2406
2430
  if (typeof window !== "undefined" && window.craft) {
2407
2431
  await window.craft.bridge?.call("fs.writeBinaryFile", { path, data: Array.from(data) });
2408
2432
  return;
@@ -2411,6 +2435,7 @@ async function writeBinaryFile(path, data) {
2411
2435
  return writeFile(path, data);
2412
2436
  }
2413
2437
  async function stat(path) {
2438
+ validatePath(path);
2414
2439
  if (typeof window !== "undefined" && window.craft) {
2415
2440
  const response = await window.craft.bridge?.call("fs.stat", { path });
2416
2441
  return {
@@ -2432,6 +2457,8 @@ async function stat(path) {
2432
2457
  };
2433
2458
  }
2434
2459
  async function copy(src, dest) {
2460
+ validatePath(src);
2461
+ validatePath(dest);
2435
2462
  if (typeof window !== "undefined" && window.craft) {
2436
2463
  await window.craft.bridge?.call("fs.copy", { src, dest });
2437
2464
  return;
@@ -2440,6 +2467,8 @@ async function copy(src, dest) {
2440
2467
  return cp(src, dest, { recursive: true });
2441
2468
  }
2442
2469
  async function move(src, dest) {
2470
+ validatePath(src);
2471
+ validatePath(dest);
2443
2472
  if (typeof window !== "undefined" && window.craft) {
2444
2473
  await window.craft.bridge?.call("fs.move", { src, dest });
2445
2474
  return;
@@ -2448,6 +2477,7 @@ async function move(src, dest) {
2448
2477
  return rename(src, dest);
2449
2478
  }
2450
2479
  function watch(path, callback) {
2480
+ validatePath(path);
2451
2481
  if (typeof window !== "undefined" && window.craft) {
2452
2482
  const watchId = Math.random().toString(36).slice(2);
2453
2483
  window.craft.bridge?.call("fs.watch", { path, watchId });
@@ -2502,6 +2532,10 @@ var db = {
2502
2532
  }
2503
2533
  };
2504
2534
  async function openDatabase(name) {
2535
+ const normalized = name.replace(/\\/g, "/");
2536
+ if (normalized.includes("/../") || normalized.startsWith("../") || normalized.endsWith("/..") || normalized === ".." || name.includes("\x00")) {
2537
+ throw new Error(`Invalid database name: "${name}" contains path traversal or invalid characters`);
2538
+ }
2505
2539
  return new Database(name);
2506
2540
  }
2507
2541
 
@@ -2620,7 +2654,8 @@ class KeyValueStore {
2620
2654
  if (row) {
2621
2655
  try {
2622
2656
  return JSON.parse(row.value);
2623
- } catch {
2657
+ } catch (err) {
2658
+ console.debug("[Craft DB] Failed to parse stored value for key, returning raw:", err);
2624
2659
  return row.value;
2625
2660
  }
2626
2661
  }
@@ -2947,11 +2982,12 @@ function bufferToHex(buffer) {
2947
2982
  }
2948
2983
  function bufferToBase64(buffer) {
2949
2984
  const bytes = new Uint8Array(buffer);
2950
- let binary = "";
2951
- for (let i = 0;i < bytes.byteLength; i++) {
2952
- binary += String.fromCharCode(bytes[i]);
2985
+ const chunks = [];
2986
+ const chunkSize = 8192;
2987
+ for (let i = 0;i < bytes.byteLength; i += chunkSize) {
2988
+ chunks.push(String.fromCharCode(...bytes.subarray(i, i + chunkSize)));
2953
2989
  }
2954
- return btoa(binary);
2990
+ return btoa(chunks.join(""));
2955
2991
  }
2956
2992
  function base64ToBuffer(base64) {
2957
2993
  const binary = atob(base64);
@@ -3036,11 +3072,12 @@ async function verifyPassword(password, hash, salt) {
3036
3072
  // src/api/process.ts
3037
3073
  var env = typeof process !== "undefined" ? process.env : {};
3038
3074
  function getPlatform() {
3039
- if (typeof window !== "undefined" && window.craft) {
3040
- const info = window.craft._platform;
3041
- if (info === "ios" || info === "android") {
3042
- return info;
3043
- }
3075
+ if (typeof globalThis !== "undefined") {
3076
+ const w = globalThis;
3077
+ if (w.craft?._platform)
3078
+ return w.craft._platform;
3079
+ if (w.__CRAFT_PLATFORM__)
3080
+ return w.__CRAFT_PLATFORM__;
3044
3081
  }
3045
3082
  if (typeof process !== "undefined" && process.platform) {
3046
3083
  return process.platform;
@@ -5365,10 +5402,12 @@ async function write(data) {
5365
5402
  }
5366
5403
  async function read() {
5367
5404
  const [text, html] = await Promise.all([
5368
- readText().catch(() => {
5405
+ readText().catch((err) => {
5406
+ console.debug("[Craft Clipboard] readText failed:", err);
5369
5407
  return;
5370
5408
  }),
5371
- readHTML().catch(() => {
5409
+ readHTML().catch((err) => {
5410
+ console.debug("[Craft Clipboard] readHTML failed:", err);
5372
5411
  return;
5373
5412
  })
5374
5413
  ]);
@@ -5822,23 +5861,26 @@ function generateCSS(config, platform) {
5822
5861
  }
5823
5862
  return baseStyles + platformStyles;
5824
5863
  }
5864
+ function escapeHtml(unsafe) {
5865
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
5866
+ }
5825
5867
  function generateHTML(config) {
5826
5868
  const renderItem = (item) => `
5827
- <div class='craft-sidebar-item${item.selected ? " selected" : ""}' data-id='${item.id}'>
5869
+ <div class='craft-sidebar-item${item.selected ? " selected" : ""}' data-id='${escapeHtml(item.id)}'>
5828
5870
  ${item.icon ? `<span class='craft-sidebar-item-icon'>${getIcon(item.icon, item.tintColor)}</span>` : ""}
5829
- <span class='craft-sidebar-item-label'>${item.label}</span>
5830
- ${item.badge !== undefined ? `<span class='craft-sidebar-item-badge'>${item.badge}</span>` : ""}
5871
+ <span class='craft-sidebar-item-label'>${escapeHtml(item.label)}</span>
5872
+ ${item.badge !== undefined ? `<span class='craft-sidebar-item-badge'>${escapeHtml(String(item.badge))}</span>` : ""}
5831
5873
  </div>
5832
5874
  `;
5833
5875
  const renderSection = (section) => `
5834
- <div class='craft-sidebar-section' data-section='${section.id}'>
5876
+ <div class='craft-sidebar-section' data-section='${escapeHtml(section.id)}'>
5835
5877
  ${section.title ? `
5836
- <div class='craft-sidebar-section-header${section.collapsed ? " collapsed" : ""}' data-section-toggle='${section.id}'>
5878
+ <div class='craft-sidebar-section-header${section.collapsed ? " collapsed" : ""}' data-section-toggle='${escapeHtml(section.id)}'>
5837
5879
  ${section.collapsible !== false ? `<svg class="craft-sidebar-section-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>` : ""}
5838
- ${section.title}
5880
+ ${escapeHtml(section.title)}
5839
5881
  </div>
5840
5882
  ` : ""}
5841
- <div class='craft-sidebar-section-items${section.collapsed ? " collapsed" : ""}' data-section-items='${section.id}'>
5883
+ <div class='craft-sidebar-section-items${section.collapsed ? " collapsed" : ""}' data-section-items='${escapeHtml(section.id)}'>
5842
5884
  ${section.items.map(renderItem).join("")}
5843
5885
  </div>
5844
5886
  </div>
@@ -5847,13 +5889,13 @@ function generateHTML(config) {
5847
5889
  <aside class='craft-sidebar'>
5848
5890
  ${config.headerTitle ? `
5849
5891
  <div class='craft-sidebar-header'>
5850
- <div class='craft-sidebar-header-title'>${config.headerTitle}</div>
5851
- ${config.headerSubtitle ? `<div class="craft-sidebar-header-subtitle">${config.headerSubtitle}</div>` : ""}
5892
+ <div class='craft-sidebar-header-title'>${escapeHtml(config.headerTitle)}</div>
5893
+ ${config.headerSubtitle ? `<div class="craft-sidebar-header-subtitle">${escapeHtml(config.headerSubtitle)}</div>` : ""}
5852
5894
  </div>
5853
5895
  ` : ""}
5854
5896
  ${config.showSearch ? `
5855
5897
  <div class='craft-sidebar-search'>
5856
- <input type='text' placeholder='${config.searchPlaceholder || "Search"}' data-sidebar-search>
5898
+ <input type='text' placeholder='${escapeHtml(config.searchPlaceholder || "Search")}' data-sidebar-search>
5857
5899
  </div>
5858
5900
  ` : ""}
5859
5901
  <div class='craft-sidebar-content'>
@@ -5864,7 +5906,7 @@ function generateHTML(config) {
5864
5906
  `;
5865
5907
  }
5866
5908
  function getIcon(name, tintColor) {
5867
- const color = tintColor || "currentColor";
5909
+ const color = escapeHtml(tintColor || "currentColor");
5868
5910
  const icons = {
5869
5911
  folder: `<svg viewBox='0 0 24 24' fill='${color}'><path d='M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z'/></svg>`,
5870
5912
  file: `<svg viewBox='0 0 24 24' fill='none' stroke='${color}' stroke-width='2'><path d='M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z'/><polyline points='13 2 13 9 20 9'/></svg>`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/craft",
3
- "version": "0.0.19",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",