@mulmoclaude/accounting-plugin 2.1.0 → 2.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 (50) hide show
  1. package/README.md +37 -0
  2. package/dist/server/context.d.ts +45 -3
  3. package/dist/server/context.d.ts.map +1 -1
  4. package/dist/server/eventPublisher.d.ts +9 -4
  5. package/dist/server/eventPublisher.d.ts.map +1 -1
  6. package/dist/server/index.d.ts +1 -0
  7. package/dist/server/index.d.ts.map +1 -1
  8. package/dist/server/router.d.ts +27 -3
  9. package/dist/server/router.d.ts.map +1 -1
  10. package/dist/server/snapshotCache.d.ts +3 -3
  11. package/dist/server/snapshotCache.d.ts.map +1 -1
  12. package/dist/server.cjs +149 -80
  13. package/dist/server.cjs.map +1 -1
  14. package/dist/server.js +149 -80
  15. package/dist/server.js.map +1 -1
  16. package/dist/shared/api.d.ts +12 -0
  17. package/dist/shared/api.d.ts.map +1 -1
  18. package/dist/shared/channels.d.ts +22 -2
  19. package/dist/shared/channels.d.ts.map +1 -1
  20. package/dist/{shared-BZAAF-I4.js → shared-BccLo0Ux.js} +39 -5
  21. package/dist/shared-BccLo0Ux.js.map +1 -0
  22. package/dist/{shared-BpFVwa6Y.cjs → shared-Dgt-i23T.cjs} +50 -4
  23. package/dist/shared-Dgt-i23T.cjs.map +1 -0
  24. package/dist/shared.cjs +3 -1
  25. package/dist/shared.js +2 -2
  26. package/dist/style.css +19 -10
  27. package/dist/vue/View.vue.d.ts +5 -0
  28. package/dist/vue/View.vue.d.ts.map +1 -1
  29. package/dist/vue/api.d.ts +132 -100
  30. package/dist/vue/api.d.ts.map +1 -1
  31. package/dist/vue/components/AccountsModal.vue.d.ts.map +1 -1
  32. package/dist/vue/components/BalanceSheet.vue.d.ts.map +1 -1
  33. package/dist/vue/components/BookSettings.vue.d.ts.map +1 -1
  34. package/dist/vue/components/JournalEntryForm.vue.d.ts.map +1 -1
  35. package/dist/vue/components/JournalList.vue.d.ts.map +1 -1
  36. package/dist/vue/components/Ledger.vue.d.ts.map +1 -1
  37. package/dist/vue/components/NewBookForm.vue.d.ts.map +1 -1
  38. package/dist/vue/components/OpeningBalancesForm.vue.d.ts.map +1 -1
  39. package/dist/vue/components/ProfitLoss.vue.d.ts.map +1 -1
  40. package/dist/vue/hostContext.d.ts +15 -0
  41. package/dist/vue/hostContext.d.ts.map +1 -1
  42. package/dist/vue/useAccountingChannel.d.ts +13 -3
  43. package/dist/vue/useAccountingChannel.d.ts.map +1 -1
  44. package/dist/vue.cjs +227 -98
  45. package/dist/vue.cjs.map +1 -1
  46. package/dist/vue.js +228 -99
  47. package/dist/vue.js.map +1 -1
  48. package/package.json +2 -2
  49. package/dist/shared-BZAAF-I4.js.map +0 -1
  50. package/dist/shared-BpFVwa6Y.cjs.map +0 -1
package/dist/server.cjs CHANGED
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_shared = require("./shared-BpFVwa6Y.cjs");
24
+ const require_shared = require("./shared-Dgt-i23T.cjs");
25
25
  let express = require("express");
26
26
  let node_crypto = require("node:crypto");
27
27
  let node_fs = require("node:fs");
@@ -84,11 +84,41 @@ function configureAccountingServer(context) {
84
84
  }
85
85
  /** Default workspace root for io calls that don't pass one explicitly.
86
86
  * Throws if the host never configured the server — a real wiring bug
87
- * (unit tests always pass an explicit root, so they never hit this). */
87
+ * (unit tests always pass an explicit root, so they never hit this)
88
+ * and also under STRICT mode (`workspaceRoot: null`), where there is no
89
+ * ambient root to fall back to and a missing option is a bug rather
90
+ * than a default. */
88
91
  function defaultWorkspaceRoot() {
89
92
  if (!deps) throw new Error("@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests");
93
+ if (deps.workspaceRoot === null) throw new Error("@mulmoclaude/accounting-plugin: this host is configured with workspaceRoot: null (explicit-root mode), so every call must pass a workspaceRoot. A call reached the engine without one.");
90
94
  return deps.workspaceRoot;
91
95
  }
96
+ /** The opaque channel scope for a root, or `null` when the host declared
97
+ * none (single-root hosts, and a multi-root host's default root). Used
98
+ * by the event publisher; never contains a path.
99
+ *
100
+ * A missing root throws under explicit-root mode, exactly as
101
+ * `defaultWorkspaceRoot()` does. Returning `null` there would be worse
102
+ * than the io-path bug it mirrors: the event would go out on the
103
+ * UNSCOPED channel name, which every project's default-scope
104
+ * subscriber receives. The publisher catches it and logs, so a dropped
105
+ * root costs one event and a warning rather than a silent cross-project
106
+ * notification. */
107
+ function channelScopeFor(workspaceRoot) {
108
+ if (!deps?.channelScopeForRoot) return null;
109
+ return deps.channelScopeForRoot(workspaceRoot ?? defaultWorkspaceRoot());
110
+ }
111
+ /** Whether the host declared project scoping at all.
112
+ *
113
+ * The difference matters where a card records the project it belongs
114
+ * to: on a scoped host, "no scope" is a REAL answer (the default root)
115
+ * and must be recorded as `null`, not left out — an omitted field means
116
+ * "ask the host what is active now", which is precisely the drift a
117
+ * pinned card exists to prevent. On an unscoped host the field is left
118
+ * out entirely and every payload stays byte-identical to today's. */
119
+ function isProjectScopedHost() {
120
+ return Boolean(deps?.channelScopeForRoot);
121
+ }
92
122
  var consoleLogger = {
93
123
  error: (namespace, msg, data) => console.error(`[${namespace}] ${msg}`, data ?? ""),
94
124
  warn: (namespace, msg, data) => console.warn(`[${namespace}] ${msg}`, data ?? ""),
@@ -1165,9 +1195,16 @@ var pubsub = null;
1165
1195
  function initAccountingEventPublisher(instance) {
1166
1196
  pubsub = instance;
1167
1197
  }
1168
- function safePublish(channel, payload) {
1198
+ /** `channelFor` is called INSIDE the guard, not before it: resolving the
1199
+ * scope can throw (a write that reached the publisher with no root
1200
+ * under explicit-root mode), and a lost event with a warning beats
1201
+ * either crashing the write or publishing to the unscoped channel that
1202
+ * every project can hear. */
1203
+ function safePublish(channelFor, payload) {
1169
1204
  if (!pubsub) return;
1205
+ let channel = "(unresolved)";
1170
1206
  try {
1207
+ channel = channelFor();
1171
1208
  pubsub.publish(channel, payload);
1172
1209
  } catch (err) {
1173
1210
  log.warn("accounting", "publish failed; subscribers will miss this event", {
@@ -1178,15 +1215,20 @@ function safePublish(channel, payload) {
1178
1215
  }
1179
1216
  /** Per-book change notification. `period` should be the entry's
1180
1217
  * YYYY-MM bucket (or the earliest invalidated month for snapshot
1181
- * events). */
1182
- function publishBookChange(bookId, payload) {
1183
- safePublish(require_shared.bookChannel(bookId), payload);
1218
+ * events).
1219
+ *
1220
+ * `workspaceRoot` is the root the write happened under; the channel
1221
+ * name is namespaced by the host's opaque scope for it (see
1222
+ * `channelScopeFor`). Omit it — as a single-root host's service calls
1223
+ * do — and the name is what it has always been. */
1224
+ function publishBookChange(bookId, payload, workspaceRoot) {
1225
+ safePublish(() => require_shared.bookChannel(bookId, channelScopeFor(workspaceRoot)), payload);
1184
1226
  }
1185
1227
  /** Fired when the *list* of books changes (createBook, deleteBook).
1186
1228
  * Payload is intentionally empty — subscribers refetch from
1187
- * /api/accounting. */
1188
- function publishBooksChanged() {
1189
- safePublish(require_shared.ACCOUNTING_BOOKS_CHANNEL, {});
1229
+ * /api/accounting. Scoped by root like `publishBookChange`. */
1230
+ function publishBooksChanged(workspaceRoot) {
1231
+ safePublish(() => require_shared.booksChannel(channelScopeFor(workspaceRoot)), {});
1190
1232
  }
1191
1233
  //#endregion
1192
1234
  //#region src/server/snapshotCache.ts
@@ -1255,18 +1297,39 @@ async function rebuildAllSnapshots(bookId, workspaceRoot) {
1255
1297
  return { rebuilt: periods };
1256
1298
  }
1257
1299
  var rebuildQueues = /* @__PURE__ */ new Map();
1300
+ /** Canonical key for the root a rebuild belongs to. An absent root
1301
+ * resolves to the host's configured one, so a call that passes it
1302
+ * explicitly and one that relies on the default share a queue. Under
1303
+ * explicit-root mode there is no default and `defaultWorkspaceRoot()`
1304
+ * throws — deliberately loud: a `cancelRebuild` / `awaitRebuildIdle`
1305
+ * that quietly keyed a queue nobody scheduled would return while the
1306
+ * real rebuild is still writing, which is how `deleteBook` races a
1307
+ * `writeSnapshot` back into existence.
1308
+ *
1309
+ * Lexical resolution only — symlinks are deliberately NOT resolved,
1310
+ * matching `canonicalRoot` in `@mulmoclaude/core`'s collection engine,
1311
+ * so one project has one identity across both packages. A host that
1312
+ * hands the engine a real path in one call and a symlink alias in the
1313
+ * next would split the queue; the fix belongs in the host's own root
1314
+ * resolution, which is where the two packages agree it lives. */
1315
+ function rootKey(workspaceRoot) {
1316
+ return node_path.default.resolve(workspaceRoot ?? defaultWorkspaceRoot());
1317
+ }
1318
+ function queueKey(bookId, workspaceRoot) {
1319
+ return `${rootKey(workspaceRoot)}\u0000${bookId}`;
1320
+ }
1258
1321
  function minPeriod(lhs, rhs) {
1259
1322
  if (lhs === null) return rhs;
1260
1323
  return lhs < rhs ? lhs : rhs;
1261
1324
  }
1262
- function isInvalidatedDuringRebuild(bookId, period) {
1263
- const queue = rebuildQueues.get(bookId);
1325
+ function isInvalidatedDuringRebuild(key, period) {
1326
+ const queue = rebuildQueues.get(key);
1264
1327
  return queue !== void 0 && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;
1265
1328
  }
1266
- function isCancelled(bookId) {
1267
- return rebuildQueues.get(bookId)?.cancelled === true;
1329
+ function isCancelled(key) {
1330
+ return rebuildQueues.get(key)?.cancelled === true;
1268
1331
  }
1269
- async function runRebuild(bookId, fromPeriod, workspaceRoot) {
1332
+ async function runRebuild(key, bookId, fromPeriod, workspaceRoot) {
1270
1333
  const startedAt = Date.now();
1271
1334
  log.info("accounting", "snapshot rebuild started", {
1272
1335
  bookId,
@@ -1275,25 +1338,25 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
1275
1338
  publishBookChange(bookId, {
1276
1339
  kind: require_shared.BOOK_EVENT_KINDS.snapshotsRebuilding,
1277
1340
  period: fromPeriod
1278
- });
1341
+ }, workspaceRoot);
1279
1342
  const targets = (await listJournalPeriods(bookId, workspaceRoot)).filter((monthKey) => monthKey >= fromPeriod);
1280
1343
  let written = 0;
1281
1344
  for (const monthKey of targets) {
1282
- if (isCancelled(bookId)) break;
1283
- if (isInvalidatedDuringRebuild(bookId, monthKey)) break;
1345
+ if (isCancelled(key)) break;
1346
+ if (isInvalidatedDuringRebuild(key, monthKey)) break;
1284
1347
  const balances = await balancesAtEndOf(bookId, monthKey, workspaceRoot);
1285
- if (isCancelled(bookId)) break;
1286
- if (isInvalidatedDuringRebuild(bookId, monthKey)) break;
1348
+ if (isCancelled(key)) break;
1349
+ if (isInvalidatedDuringRebuild(key, monthKey)) break;
1287
1350
  await writeSnapshot(bookId, {
1288
1351
  period: monthKey,
1289
1352
  balances,
1290
1353
  builtAt: (/* @__PURE__ */ new Date()).toISOString()
1291
1354
  }, workspaceRoot);
1292
- if (isCancelled(bookId)) {
1355
+ if (isCancelled(key)) {
1293
1356
  await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
1294
1357
  break;
1295
1358
  }
1296
- if (isInvalidatedDuringRebuild(bookId, monthKey)) {
1359
+ if (isInvalidatedDuringRebuild(key, monthKey)) {
1297
1360
  await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
1298
1361
  break;
1299
1362
  }
@@ -1301,7 +1364,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
1301
1364
  publishBookChange(bookId, {
1302
1365
  kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady,
1303
1366
  period: monthKey
1304
- });
1367
+ }, workspaceRoot);
1305
1368
  }
1306
1369
  log.info("accounting", "snapshot rebuild done", {
1307
1370
  bookId,
@@ -1309,7 +1372,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
1309
1372
  durationMs: Date.now() - startedAt
1310
1373
  });
1311
1374
  }
1312
- function startRebuild(bookId, fromPeriod, workspaceRoot) {
1375
+ function startRebuild(key, bookId, fromPeriod, workspaceRoot) {
1313
1376
  const entry = {
1314
1377
  running: Promise.resolve(),
1315
1378
  pendingFromPeriod: null,
@@ -1318,27 +1381,27 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
1318
1381
  runningFromPeriod: fromPeriod,
1319
1382
  cancelled: false
1320
1383
  };
1321
- entry.running = runRebuild(bookId, fromPeriod, workspaceRoot).catch((err) => {
1384
+ entry.running = runRebuild(key, bookId, fromPeriod, workspaceRoot).catch((err) => {
1322
1385
  log.error("accounting", "snapshot rebuild failed", {
1323
1386
  bookId,
1324
1387
  fromPeriod,
1325
1388
  error: require_shared.errorMessage(err)
1326
1389
  });
1327
1390
  }).then(() => {
1328
- const current = rebuildQueues.get(bookId);
1391
+ const current = rebuildQueues.get(key);
1329
1392
  if (!current) return;
1330
1393
  if (current.cancelled) {
1331
- rebuildQueues.delete(bookId);
1394
+ rebuildQueues.delete(key);
1332
1395
  return;
1333
1396
  }
1334
1397
  if (current.pendingFromPeriod !== null) {
1335
1398
  const nextFrom = current.pendingFromPeriod;
1336
1399
  const nextRoot = current.pendingWorkspaceRoot;
1337
1400
  const carriedCount = current.coalescedWriteCount;
1338
- const successor = startRebuild(bookId, nextFrom, nextRoot);
1401
+ const successor = startRebuild(key, bookId, nextFrom, nextRoot);
1339
1402
  successor.coalescedWriteCount += carriedCount;
1340
- rebuildQueues.set(bookId, successor);
1341
- } else rebuildQueues.delete(bookId);
1403
+ rebuildQueues.set(key, successor);
1404
+ } else rebuildQueues.delete(key);
1342
1405
  });
1343
1406
  return entry;
1344
1407
  }
@@ -1347,9 +1410,10 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
1347
1410
  * follow-up rebuild that covers the minimum `fromPeriod` seen.
1348
1411
  * Returns immediately — the rebuild runs on its own promise chain. */
1349
1412
  function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
1350
- const existing = rebuildQueues.get(bookId);
1413
+ const key = queueKey(bookId, workspaceRoot);
1414
+ const existing = rebuildQueues.get(key);
1351
1415
  if (!existing) {
1352
- rebuildQueues.set(bookId, startRebuild(bookId, fromPeriod, workspaceRoot));
1416
+ rebuildQueues.set(key, startRebuild(key, bookId, fromPeriod, workspaceRoot));
1353
1417
  return;
1354
1418
  }
1355
1419
  existing.pendingFromPeriod = minPeriod(existing.pendingFromPeriod, fromPeriod);
@@ -1360,9 +1424,10 @@ function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
1360
1424
  * `bookId`. Also called by `deleteBook` after `cancelRebuild` to
1361
1425
  * ensure a previously running rebuild has fully stopped before the
1362
1426
  * caller removes the book's directory on disk. */
1363
- async function awaitRebuildIdle(bookId) {
1364
- while (rebuildQueues.has(bookId)) {
1365
- const entry = rebuildQueues.get(bookId);
1427
+ async function awaitRebuildIdle(bookId, workspaceRoot) {
1428
+ const key = queueKey(bookId, workspaceRoot);
1429
+ while (rebuildQueues.has(key)) {
1430
+ const entry = rebuildQueues.get(key);
1366
1431
  if (!entry) return;
1367
1432
  await entry.running;
1368
1433
  }
@@ -1372,8 +1437,8 @@ async function awaitRebuildIdle(bookId) {
1372
1437
  * `removeBookDir` cannot race with a `writeSnapshot` that would
1373
1438
  * re-create the directory tree. Pair with `awaitRebuildIdle(bookId)`
1374
1439
  * to wait for the in-flight rebuild to finish bailing. */
1375
- function cancelRebuild(bookId) {
1376
- const entry = rebuildQueues.get(bookId);
1440
+ function cancelRebuild(bookId, workspaceRoot) {
1441
+ const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));
1377
1442
  if (!entry) return;
1378
1443
  entry.cancelled = true;
1379
1444
  entry.pendingFromPeriod = null;
@@ -1727,7 +1792,7 @@ async function createBook(input, workspaceRoot) {
1727
1792
  await ensureBookDir(bookId, workspaceRoot);
1728
1793
  await writeAccounts(bookId, [...DEFAULT_ACCOUNTS], workspaceRoot);
1729
1794
  await writeConfig({ books: [...config.books, book] }, workspaceRoot);
1730
- publishBooksChanged();
1795
+ publishBooksChanged(workspaceRoot);
1731
1796
  return { book };
1732
1797
  }
1733
1798
  async function updateBook(input, workspaceRoot) {
@@ -1744,7 +1809,7 @@ async function updateBook(input, workspaceRoot) {
1744
1809
  };
1745
1810
  if (country === "") delete next.country;
1746
1811
  await writeConfig({ books: config.books.map((book) => book.id === input.bookId ? next : book) }, workspaceRoot);
1747
- publishBooksChanged();
1812
+ publishBooksChanged(workspaceRoot);
1748
1813
  return { book: next };
1749
1814
  }
1750
1815
  async function deleteBook(input, workspaceRoot) {
@@ -1752,11 +1817,11 @@ async function deleteBook(input, workspaceRoot) {
1752
1817
  const config = await loadOrInitConfig(workspaceRoot);
1753
1818
  const target = findBook(config, input.bookId);
1754
1819
  if (!target) throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);
1755
- cancelRebuild(input.bookId);
1756
- await awaitRebuildIdle(input.bookId);
1820
+ cancelRebuild(input.bookId, workspaceRoot);
1821
+ await awaitRebuildIdle(input.bookId, workspaceRoot);
1757
1822
  await removeBookDir(input.bookId, workspaceRoot);
1758
1823
  await writeConfig({ books: config.books.filter((book) => book.id !== input.bookId) }, workspaceRoot);
1759
- publishBooksChanged();
1824
+ publishBooksChanged(workspaceRoot);
1760
1825
  return {
1761
1826
  deletedBookId: input.bookId,
1762
1827
  deletedBookName: target.name
@@ -1806,7 +1871,7 @@ async function upsertAccount(input, workspaceRoot) {
1806
1871
  scheduleRebuild(bookId, "0000-00", workspaceRoot);
1807
1872
  await invalidateAllSnapshots(bookId, workspaceRoot);
1808
1873
  }
1809
- publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.accounts });
1874
+ publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.accounts }, workspaceRoot);
1810
1875
  return {
1811
1876
  bookId,
1812
1877
  account,
@@ -1855,7 +1920,7 @@ async function addEntries(input, workspaceRoot) {
1855
1920
  publishBookChange(bookId, {
1856
1921
  kind: require_shared.BOOK_EVENT_KINDS.journal,
1857
1922
  period: earliestPeriod
1858
- });
1923
+ }, workspaceRoot);
1859
1924
  return {
1860
1925
  bookId,
1861
1926
  entries: built
@@ -1884,7 +1949,7 @@ async function voidEntry(input, workspaceRoot) {
1884
1949
  publishBookChange(bookId, {
1885
1950
  kind: require_shared.BOOK_EVENT_KINDS.journal,
1886
1951
  period: fromPeriod
1887
- });
1952
+ }, workspaceRoot);
1888
1953
  return {
1889
1954
  bookId,
1890
1955
  reverseEntry: reverse,
@@ -1948,7 +2013,7 @@ async function setOpeningBalances(input, workspaceRoot) {
1948
2013
  await appendJournal(bookId, opening, workspaceRoot);
1949
2014
  scheduleRebuild(bookId, "0000-00", workspaceRoot);
1950
2015
  await invalidateAllSnapshots(bookId, workspaceRoot);
1951
- publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.opening });
2016
+ publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.opening }, workspaceRoot);
1952
2017
  return {
1953
2018
  bookId,
1954
2019
  openingEntry: opening,
@@ -2083,7 +2148,7 @@ async function getTimeSeriesReport(input, workspaceRoot) {
2083
2148
  async function rebuildSnapshots(input, workspaceRoot) {
2084
2149
  const bookId = resolveBookId(await loadOrInitConfig(workspaceRoot), input.bookId);
2085
2150
  const result = await rebuildAllSnapshots(bookId, workspaceRoot);
2086
- publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady });
2151
+ publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady }, workspaceRoot);
2087
2152
  return {
2088
2153
  bookId,
2089
2154
  rebuilt: result.rebuilt
@@ -2110,19 +2175,21 @@ function asyncHandler(namespace, fallbackMessage, handler) {
2110
2175
  }
2111
2176
  //#endregion
2112
2177
  //#region src/server/router.ts
2113
- async function handleOpenBook(rest) {
2178
+ async function handleOpenBook(rest, root) {
2114
2179
  if (typeof rest.bookId !== "string" || rest.bookId === "") throw new AccountingError(400, "openBook: bookId is required. Call 'getBooks' to enumerate, or 'createBook' first on a fresh workspace.");
2115
- const list = await listBooks();
2180
+ const list = await listBooks(root);
2116
2181
  if (!list.books.some((book) => book.id === rest.bookId)) throw new AccountingError(404, `openBook: book ${JSON.stringify(rest.bookId)} not found`);
2117
2182
  const initialTab = typeof rest.initialTab === "string" ? rest.initialTab : void 0;
2183
+ const scopeField = isProjectScopedHost() ? { scope: channelScopeFor(root) } : {};
2118
2184
  return {
2119
2185
  kind: "accounting-app",
2120
2186
  bookId: rest.bookId,
2121
2187
  initialTab,
2122
- books: list.books
2188
+ books: list.books,
2189
+ ...scopeField
2123
2190
  };
2124
2191
  }
2125
- async function handleGetReport(rest) {
2192
+ async function handleGetReport(rest, root) {
2126
2193
  const kind = typeof rest.kind === "string" ? rest.kind : "";
2127
2194
  const periodInput = optionalReportPeriod(rest.period);
2128
2195
  const bookId = optionalString(rest.bookId);
@@ -2133,14 +2200,14 @@ async function handleGetReport(rest) {
2133
2200
  return getBalanceSheetReport({
2134
2201
  bookId,
2135
2202
  period: periodInput
2136
- });
2203
+ }, root);
2137
2204
  }
2138
2205
  if (kind === "pl") {
2139
2206
  if (!periodInput) throw periodRequired("getReport pl");
2140
2207
  return getProfitLossReport({
2141
2208
  bookId,
2142
2209
  period: periodInput
2143
- });
2210
+ }, root);
2144
2211
  }
2145
2212
  if (kind === "ledger") {
2146
2213
  if (typeof rest.accountCode !== "string" || rest.accountCode === "") throw new AccountingError(400, "getReport ledger: accountCode is required");
@@ -2148,79 +2215,79 @@ async function handleGetReport(rest) {
2148
2215
  bookId,
2149
2216
  accountCode: rest.accountCode,
2150
2217
  period: periodInput
2151
- });
2218
+ }, root);
2152
2219
  }
2153
2220
  throw new AccountingError(400, `getReport: unknown kind ${JSON.stringify(kind)}`);
2154
2221
  }
2155
2222
  var ACTION_HANDLERS = {
2156
2223
  [require_shared.ACCOUNTING_ACTIONS.openBook]: handleOpenBook,
2157
- [require_shared.ACCOUNTING_ACTIONS.getBooks]: () => listBooks(),
2158
- [require_shared.ACCOUNTING_ACTIONS.createBook]: async (rest) => {
2224
+ [require_shared.ACCOUNTING_ACTIONS.getBooks]: (_rest, root) => listBooks(root),
2225
+ [require_shared.ACCOUNTING_ACTIONS.createBook]: async (rest, root) => {
2159
2226
  const result = await createBook({
2160
2227
  name: typeof rest.name === "string" ? rest.name : "",
2161
2228
  currency: typeof rest.currency === "string" ? rest.currency : void 0,
2162
2229
  country: typeof rest.country === "string" ? rest.country : void 0,
2163
2230
  fiscalYearEnd: rest.fiscalYearEnd
2164
- });
2231
+ }, root);
2165
2232
  return {
2166
2233
  bookId: result.book.id,
2167
2234
  ...result
2168
2235
  };
2169
2236
  },
2170
- [require_shared.ACCOUNTING_ACTIONS.updateBook]: async (rest) => {
2237
+ [require_shared.ACCOUNTING_ACTIONS.updateBook]: async (rest, root) => {
2171
2238
  const result = await updateBook({
2172
2239
  bookId: typeof rest.bookId === "string" ? rest.bookId : "",
2173
2240
  name: typeof rest.name === "string" ? rest.name : void 0,
2174
2241
  country: typeof rest.country === "string" ? rest.country : void 0,
2175
2242
  fiscalYearEnd: rest.fiscalYearEnd
2176
- });
2243
+ }, root);
2177
2244
  return {
2178
2245
  bookId: result.book.id,
2179
2246
  ...result
2180
2247
  };
2181
2248
  },
2182
- [require_shared.ACCOUNTING_ACTIONS.deleteBook]: (rest) => deleteBook({
2249
+ [require_shared.ACCOUNTING_ACTIONS.deleteBook]: (rest, root) => deleteBook({
2183
2250
  bookId: typeof rest.bookId === "string" ? rest.bookId : "",
2184
2251
  confirm: rest.confirm === true
2185
- }),
2186
- [require_shared.ACCOUNTING_ACTIONS.getAccounts]: (rest) => listAccounts({ bookId: optionalString(rest.bookId) }),
2187
- [require_shared.ACCOUNTING_ACTIONS.upsertAccount]: (rest) => upsertAccount({
2252
+ }, root),
2253
+ [require_shared.ACCOUNTING_ACTIONS.getAccounts]: (rest, root) => listAccounts({ bookId: optionalString(rest.bookId) }, root),
2254
+ [require_shared.ACCOUNTING_ACTIONS.upsertAccount]: (rest, root) => upsertAccount({
2188
2255
  bookId: optionalString(rest.bookId),
2189
2256
  account: rest.account
2190
- }),
2191
- [require_shared.ACCOUNTING_ACTIONS.addEntries]: (rest) => addEntries({
2257
+ }, root),
2258
+ [require_shared.ACCOUNTING_ACTIONS.addEntries]: (rest, root) => addEntries({
2192
2259
  bookId: optionalString(rest.bookId),
2193
2260
  entries: rest.entries
2194
- }),
2195
- [require_shared.ACCOUNTING_ACTIONS.voidEntry]: (rest) => voidEntry({
2261
+ }, root),
2262
+ [require_shared.ACCOUNTING_ACTIONS.voidEntry]: (rest, root) => voidEntry({
2196
2263
  bookId: optionalString(rest.bookId),
2197
2264
  entryId: typeof rest.entryId === "string" ? rest.entryId : "",
2198
2265
  reason: optionalString(rest.reason),
2199
2266
  voidDate: optionalString(rest.voidDate)
2200
- }),
2201
- [require_shared.ACCOUNTING_ACTIONS.getJournalEntries]: (rest) => listEntries({
2267
+ }, root),
2268
+ [require_shared.ACCOUNTING_ACTIONS.getJournalEntries]: (rest, root) => listEntries({
2202
2269
  bookId: optionalString(rest.bookId),
2203
2270
  from: optionalString(rest.from),
2204
2271
  to: optionalString(rest.to),
2205
2272
  accountCode: optionalString(rest.accountCode)
2206
- }),
2207
- [require_shared.ACCOUNTING_ACTIONS.getOpeningBalances]: (rest) => getOpeningBalances({ bookId: optionalString(rest.bookId) }),
2208
- [require_shared.ACCOUNTING_ACTIONS.setOpeningBalances]: (rest) => setOpeningBalances({
2273
+ }, root),
2274
+ [require_shared.ACCOUNTING_ACTIONS.getOpeningBalances]: (rest, root) => getOpeningBalances({ bookId: optionalString(rest.bookId) }, root),
2275
+ [require_shared.ACCOUNTING_ACTIONS.setOpeningBalances]: (rest, root) => setOpeningBalances({
2209
2276
  bookId: optionalString(rest.bookId),
2210
2277
  asOfDate: typeof rest.asOfDate === "string" ? rest.asOfDate : "",
2211
2278
  lines: rest.lines ?? [],
2212
2279
  memo: optionalString(rest.memo)
2213
- }),
2280
+ }, root),
2214
2281
  [require_shared.ACCOUNTING_ACTIONS.getReport]: handleGetReport,
2215
- [require_shared.ACCOUNTING_ACTIONS.getTimeSeries]: (rest) => getTimeSeriesReport({
2282
+ [require_shared.ACCOUNTING_ACTIONS.getTimeSeries]: (rest, root) => getTimeSeriesReport({
2216
2283
  bookId: optionalString(rest.bookId),
2217
2284
  metric: rest.metric,
2218
2285
  granularity: rest.granularity,
2219
2286
  from: rest.from,
2220
2287
  to: rest.to,
2221
2288
  accountCode: rest.accountCode
2222
- }),
2223
- [require_shared.ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest) => rebuildSnapshots({ bookId: optionalString(rest.bookId) })
2289
+ }, root),
2290
+ [require_shared.ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest, root) => rebuildSnapshots({ bookId: optionalString(rest.bookId) }, root)
2224
2291
  };
2225
2292
  var PREVIEW_ACTIONS = /* @__PURE__ */ new Set([
2226
2293
  require_shared.ACCOUNTING_ACTIONS.openBook,
@@ -2297,11 +2364,11 @@ function previewMessage(action, fields) {
2297
2364
  const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);
2298
2365
  return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;
2299
2366
  }
2300
- async function dispatch(body) {
2367
+ async function dispatch(body, root) {
2301
2368
  const { action, ...rest } = body;
2302
2369
  const handler = ownEntry(ACTION_HANDLERS, action);
2303
2370
  if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);
2304
- const result = await handler(rest);
2371
+ const result = await handler(rest, root);
2305
2372
  const handlerFields = require_shared.isRecord(result) ? result : { value: result };
2306
2373
  const dataField = PREVIEW_ACTIONS.has(action) ? { data: {
2307
2374
  action,
@@ -2318,8 +2385,9 @@ async function dispatch(body) {
2318
2385
  /** Build the accounting Express router. The host injects its workspace
2319
2386
  * root + logger via `configureAccountingServer(...)` and pub/sub via
2320
2387
  * `initAccountingEventPublisher(...)`, then mounts the returned router
2321
- * with `app.use(...)`. */
2322
- function createAccountingRouter() {
2388
+ * with `app.use(...)`. Pass `resolveWorkspaceRoot` to serve more than
2389
+ * one root; omit it and every request uses the configured one. */
2390
+ function createAccountingRouter(options = {}) {
2323
2391
  const router = (0, express.Router)();
2324
2392
  router.post(require_shared.ACCOUNTING_API.dispatch.path, asyncHandler("accounting", "accounting dispatch failed", async (req, res) => {
2325
2393
  const { body } = req;
@@ -2331,7 +2399,8 @@ function createAccountingRouter() {
2331
2399
  const { action } = body;
2332
2400
  log.info("accounting", "POST dispatch: start", { action });
2333
2401
  try {
2334
- const result = await dispatch(body);
2402
+ const root = options.resolveWorkspaceRoot?.(req);
2403
+ const result = await dispatch(body, root);
2335
2404
  log.info("accounting", "POST dispatch: ok", { action });
2336
2405
  res.json(result);
2337
2406
  } catch (err) {