noxctl 0.7.4 → 0.9.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 (272) hide show
  1. package/README.md +50 -4
  2. package/dist/auth.d.ts +18 -1
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +129 -15
  5. package/dist/auth.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +144 -33
  8. package/dist/cli.js.map +1 -1
  9. package/dist/credentials-store.d.ts +3 -0
  10. package/dist/credentials-store.d.ts.map +1 -1
  11. package/dist/credentials-store.js +54 -10
  12. package/dist/credentials-store.js.map +1 -1
  13. package/dist/embedded.d.ts +15 -0
  14. package/dist/embedded.d.ts.map +1 -0
  15. package/dist/embedded.js +11 -0
  16. package/dist/embedded.js.map +1 -0
  17. package/dist/fortnox-client.d.ts +40 -25
  18. package/dist/fortnox-client.d.ts.map +1 -1
  19. package/dist/fortnox-client.js +165 -51
  20. package/dist/fortnox-client.js.map +1 -1
  21. package/dist/index.d.ts +7 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +50 -35
  24. package/dist/index.js.map +1 -1
  25. package/dist/keychain-target.d.ts +3 -0
  26. package/dist/keychain-target.d.ts.map +1 -1
  27. package/dist/keychain-target.js +28 -7
  28. package/dist/keychain-target.js.map +1 -1
  29. package/dist/operations/absencetransactions.d.ts +9 -6
  30. package/dist/operations/absencetransactions.d.ts.map +1 -1
  31. package/dist/operations/absencetransactions.js +42 -33
  32. package/dist/operations/absencetransactions.js.map +1 -1
  33. package/dist/operations/accounts.d.ts +6 -3
  34. package/dist/operations/accounts.d.ts.map +1 -1
  35. package/dist/operations/accounts.js +28 -24
  36. package/dist/operations/accounts.js.map +1 -1
  37. package/dist/operations/analytics.d.ts +35 -0
  38. package/dist/operations/analytics.d.ts.map +1 -1
  39. package/dist/operations/analytics.js +59 -35
  40. package/dist/operations/analytics.js.map +1 -1
  41. package/dist/operations/articles.d.ts +9 -6
  42. package/dist/operations/articles.d.ts.map +1 -1
  43. package/dist/operations/articles.js +36 -32
  44. package/dist/operations/articles.js.map +1 -1
  45. package/dist/operations/attendancetransactions.d.ts +9 -6
  46. package/dist/operations/attendancetransactions.d.ts.map +1 -1
  47. package/dist/operations/attendancetransactions.js +45 -36
  48. package/dist/operations/attendancetransactions.js.map +1 -1
  49. package/dist/operations/company.d.ts +5 -1
  50. package/dist/operations/company.d.ts.map +1 -1
  51. package/dist/operations/company.js +8 -4
  52. package/dist/operations/company.js.map +1 -1
  53. package/dist/operations/contracts.d.ts +12 -9
  54. package/dist/operations/contracts.d.ts.map +1 -1
  55. package/dist/operations/contracts.js +59 -47
  56. package/dist/operations/contracts.js.map +1 -1
  57. package/dist/operations/costcenters.d.ts +10 -7
  58. package/dist/operations/costcenters.d.ts.map +1 -1
  59. package/dist/operations/costcenters.js +39 -35
  60. package/dist/operations/costcenters.js.map +1 -1
  61. package/dist/operations/customers.d.ts +9 -6
  62. package/dist/operations/customers.d.ts.map +1 -1
  63. package/dist/operations/customers.js +46 -42
  64. package/dist/operations/customers.js.map +1 -1
  65. package/dist/operations/employees.d.ts +9 -6
  66. package/dist/operations/employees.d.ts.map +1 -1
  67. package/dist/operations/employees.js +35 -31
  68. package/dist/operations/employees.js.map +1 -1
  69. package/dist/operations/financial-reports.d.ts +7 -2
  70. package/dist/operations/financial-reports.d.ts.map +1 -1
  71. package/dist/operations/financial-reports.js +169 -165
  72. package/dist/operations/financial-reports.js.map +1 -1
  73. package/dist/operations/financial-years.d.ts +8 -5
  74. package/dist/operations/financial-years.d.ts.map +1 -1
  75. package/dist/operations/financial-years.js +33 -29
  76. package/dist/operations/financial-years.js.map +1 -1
  77. package/dist/operations/index.d.ts +311 -0
  78. package/dist/operations/index.d.ts.map +1 -0
  79. package/dist/operations/index.js +64 -0
  80. package/dist/operations/index.js.map +1 -0
  81. package/dist/operations/invoice-payments.d.ts +10 -7
  82. package/dist/operations/invoice-payments.d.ts.map +1 -1
  83. package/dist/operations/invoice-payments.js +45 -35
  84. package/dist/operations/invoice-payments.js.map +1 -1
  85. package/dist/operations/invoices.d.ts +34 -30
  86. package/dist/operations/invoices.d.ts.map +1 -1
  87. package/dist/operations/invoices.js +225 -113
  88. package/dist/operations/invoices.js.map +1 -1
  89. package/dist/operations/offers.d.ts +11 -8
  90. package/dist/operations/offers.d.ts.map +1 -1
  91. package/dist/operations/offers.js +54 -43
  92. package/dist/operations/offers.js.map +1 -1
  93. package/dist/operations/orders.d.ts +10 -7
  94. package/dist/operations/orders.d.ts.map +1 -1
  95. package/dist/operations/orders.js +43 -39
  96. package/dist/operations/orders.js.map +1 -1
  97. package/dist/operations/pricelists.d.ts +16 -10
  98. package/dist/operations/pricelists.d.ts.map +1 -1
  99. package/dist/operations/pricelists.js +56 -48
  100. package/dist/operations/pricelists.js.map +1 -1
  101. package/dist/operations/projects.d.ts +9 -6
  102. package/dist/operations/projects.d.ts.map +1 -1
  103. package/dist/operations/projects.js +34 -30
  104. package/dist/operations/projects.js.map +1 -1
  105. package/dist/operations/recurrings.d.ts +14 -10
  106. package/dist/operations/recurrings.d.ts.map +1 -1
  107. package/dist/operations/recurrings.js +74 -59
  108. package/dist/operations/recurrings.js.map +1 -1
  109. package/dist/operations/salarytransactions.d.ts +9 -6
  110. package/dist/operations/salarytransactions.d.ts.map +1 -1
  111. package/dist/operations/salarytransactions.js +41 -32
  112. package/dist/operations/salarytransactions.js.map +1 -1
  113. package/dist/operations/scheduletimes.d.ts +7 -3
  114. package/dist/operations/scheduletimes.d.ts.map +1 -1
  115. package/dist/operations/scheduletimes.js +26 -22
  116. package/dist/operations/scheduletimes.js.map +1 -1
  117. package/dist/operations/supplier-invoice-payments.d.ts +9 -6
  118. package/dist/operations/supplier-invoice-payments.d.ts.map +1 -1
  119. package/dist/operations/supplier-invoice-payments.js +38 -29
  120. package/dist/operations/supplier-invoice-payments.js.map +1 -1
  121. package/dist/operations/supplier-invoices.d.ts +9 -6
  122. package/dist/operations/supplier-invoices.d.ts.map +1 -1
  123. package/dist/operations/supplier-invoices.js +41 -32
  124. package/dist/operations/supplier-invoices.js.map +1 -1
  125. package/dist/operations/suppliers.d.ts +9 -6
  126. package/dist/operations/suppliers.d.ts.map +1 -1
  127. package/dist/operations/suppliers.js +36 -32
  128. package/dist/operations/suppliers.js.map +1 -1
  129. package/dist/operations/tax.d.ts +5 -1
  130. package/dist/operations/tax.d.ts.map +1 -1
  131. package/dist/operations/tax.js +43 -39
  132. package/dist/operations/tax.js.map +1 -1
  133. package/dist/operations/taxreductions.d.ts +8 -5
  134. package/dist/operations/taxreductions.d.ts.map +1 -1
  135. package/dist/operations/taxreductions.js +28 -24
  136. package/dist/operations/taxreductions.js.map +1 -1
  137. package/dist/operations/vouchers.d.ts +26 -8
  138. package/dist/operations/vouchers.d.ts.map +1 -1
  139. package/dist/operations/vouchers.js +174 -122
  140. package/dist/operations/vouchers.js.map +1 -1
  141. package/dist/schema-audit-mappings.d.ts +9 -0
  142. package/dist/schema-audit-mappings.d.ts.map +1 -0
  143. package/dist/schema-audit-mappings.js +45 -0
  144. package/dist/schema-audit-mappings.js.map +1 -0
  145. package/dist/schema-audit.d.ts +45 -0
  146. package/dist/schema-audit.d.ts.map +1 -0
  147. package/dist/schema-audit.js +132 -0
  148. package/dist/schema-audit.js.map +1 -0
  149. package/dist/scope-probes.d.ts.map +1 -1
  150. package/dist/scope-probes.js +1 -0
  151. package/dist/scope-probes.js.map +1 -1
  152. package/dist/strict-mcp-server.d.ts +17 -0
  153. package/dist/strict-mcp-server.d.ts.map +1 -0
  154. package/dist/strict-mcp-server.js +35 -0
  155. package/dist/strict-mcp-server.js.map +1 -0
  156. package/dist/tools/absencetransactions.d.ts +2 -1
  157. package/dist/tools/absencetransactions.d.ts.map +1 -1
  158. package/dist/tools/absencetransactions.js +3 -2
  159. package/dist/tools/absencetransactions.js.map +1 -1
  160. package/dist/tools/analytics.d.ts +2 -1
  161. package/dist/tools/analytics.d.ts.map +1 -1
  162. package/dist/tools/analytics.js +3 -2
  163. package/dist/tools/analytics.js.map +1 -1
  164. package/dist/tools/articles.d.ts +2 -1
  165. package/dist/tools/articles.d.ts.map +1 -1
  166. package/dist/tools/articles.js +3 -2
  167. package/dist/tools/articles.js.map +1 -1
  168. package/dist/tools/attendancetransactions.d.ts +2 -1
  169. package/dist/tools/attendancetransactions.d.ts.map +1 -1
  170. package/dist/tools/attendancetransactions.js +3 -2
  171. package/dist/tools/attendancetransactions.js.map +1 -1
  172. package/dist/tools/bookkeeping.d.ts +2 -1
  173. package/dist/tools/bookkeeping.d.ts.map +1 -1
  174. package/dist/tools/bookkeeping.js +83 -5
  175. package/dist/tools/bookkeeping.js.map +1 -1
  176. package/dist/tools/company.d.ts +2 -1
  177. package/dist/tools/company.d.ts.map +1 -1
  178. package/dist/tools/company.js +3 -2
  179. package/dist/tools/company.js.map +1 -1
  180. package/dist/tools/contracts.d.ts +2 -1
  181. package/dist/tools/contracts.d.ts.map +1 -1
  182. package/dist/tools/contracts.js +3 -2
  183. package/dist/tools/contracts.js.map +1 -1
  184. package/dist/tools/costcenters.d.ts +2 -1
  185. package/dist/tools/costcenters.d.ts.map +1 -1
  186. package/dist/tools/costcenters.js +3 -2
  187. package/dist/tools/costcenters.js.map +1 -1
  188. package/dist/tools/customers.d.ts +2 -1
  189. package/dist/tools/customers.d.ts.map +1 -1
  190. package/dist/tools/customers.js +3 -2
  191. package/dist/tools/customers.js.map +1 -1
  192. package/dist/tools/employees.d.ts +2 -1
  193. package/dist/tools/employees.d.ts.map +1 -1
  194. package/dist/tools/employees.js +3 -2
  195. package/dist/tools/employees.js.map +1 -1
  196. package/dist/tools/financial-reports.d.ts +2 -1
  197. package/dist/tools/financial-reports.d.ts.map +1 -1
  198. package/dist/tools/financial-reports.js +3 -2
  199. package/dist/tools/financial-reports.js.map +1 -1
  200. package/dist/tools/financial-years.d.ts +2 -1
  201. package/dist/tools/financial-years.d.ts.map +1 -1
  202. package/dist/tools/financial-years.js +3 -2
  203. package/dist/tools/financial-years.js.map +1 -1
  204. package/dist/tools/invoice-payments.d.ts +2 -1
  205. package/dist/tools/invoice-payments.d.ts.map +1 -1
  206. package/dist/tools/invoice-payments.js +3 -2
  207. package/dist/tools/invoice-payments.js.map +1 -1
  208. package/dist/tools/invoices.d.ts +2 -1
  209. package/dist/tools/invoices.d.ts.map +1 -1
  210. package/dist/tools/invoices.js +100 -21
  211. package/dist/tools/invoices.js.map +1 -1
  212. package/dist/tools/offers.d.ts +2 -1
  213. package/dist/tools/offers.d.ts.map +1 -1
  214. package/dist/tools/offers.js +50 -19
  215. package/dist/tools/offers.js.map +1 -1
  216. package/dist/tools/orders.d.ts +2 -1
  217. package/dist/tools/orders.d.ts.map +1 -1
  218. package/dist/tools/orders.js +58 -18
  219. package/dist/tools/orders.js.map +1 -1
  220. package/dist/tools/pricelists.d.ts +2 -1
  221. package/dist/tools/pricelists.d.ts.map +1 -1
  222. package/dist/tools/pricelists.js +3 -2
  223. package/dist/tools/pricelists.js.map +1 -1
  224. package/dist/tools/projects.d.ts +2 -1
  225. package/dist/tools/projects.d.ts.map +1 -1
  226. package/dist/tools/projects.js +3 -2
  227. package/dist/tools/projects.js.map +1 -1
  228. package/dist/tools/recurrings.d.ts +2 -1
  229. package/dist/tools/recurrings.d.ts.map +1 -1
  230. package/dist/tools/recurrings.js +3 -2
  231. package/dist/tools/recurrings.js.map +1 -1
  232. package/dist/tools/salarytransactions.d.ts +2 -1
  233. package/dist/tools/salarytransactions.d.ts.map +1 -1
  234. package/dist/tools/salarytransactions.js +3 -2
  235. package/dist/tools/salarytransactions.js.map +1 -1
  236. package/dist/tools/scheduletimes.d.ts +2 -1
  237. package/dist/tools/scheduletimes.d.ts.map +1 -1
  238. package/dist/tools/scheduletimes.js +3 -2
  239. package/dist/tools/scheduletimes.js.map +1 -1
  240. package/dist/tools/status.d.ts.map +1 -1
  241. package/dist/tools/status.js +5 -4
  242. package/dist/tools/status.js.map +1 -1
  243. package/dist/tools/supplier-invoice-payments.d.ts +2 -1
  244. package/dist/tools/supplier-invoice-payments.d.ts.map +1 -1
  245. package/dist/tools/supplier-invoice-payments.js +3 -2
  246. package/dist/tools/supplier-invoice-payments.js.map +1 -1
  247. package/dist/tools/supplier-invoices.d.ts +2 -1
  248. package/dist/tools/supplier-invoices.d.ts.map +1 -1
  249. package/dist/tools/supplier-invoices.js +47 -6
  250. package/dist/tools/supplier-invoices.js.map +1 -1
  251. package/dist/tools/suppliers.d.ts +2 -1
  252. package/dist/tools/suppliers.d.ts.map +1 -1
  253. package/dist/tools/suppliers.js +3 -2
  254. package/dist/tools/suppliers.js.map +1 -1
  255. package/dist/tools/tax.d.ts +2 -1
  256. package/dist/tools/tax.d.ts.map +1 -1
  257. package/dist/tools/tax.js +3 -2
  258. package/dist/tools/tax.js.map +1 -1
  259. package/dist/tools/taxreductions.d.ts +2 -1
  260. package/dist/tools/taxreductions.d.ts.map +1 -1
  261. package/dist/tools/taxreductions.js +3 -2
  262. package/dist/tools/taxreductions.js.map +1 -1
  263. package/dist/version.d.ts +2 -0
  264. package/dist/version.d.ts.map +1 -0
  265. package/dist/version.js +3 -0
  266. package/dist/version.js.map +1 -0
  267. package/dist/views.d.ts +2 -0
  268. package/dist/views.d.ts.map +1 -1
  269. package/dist/views.js +11 -0
  270. package/dist/views.js.map +1 -1
  271. package/docs/embedded.md +60 -0
  272. package/package.json +23 -4
package/dist/cli.js CHANGED
@@ -7,7 +7,8 @@ import { readActivePointer, readActivePointerOutcome, writeActivePointer, delete
7
7
  import { setResolvedProfile } from './auth.js';
8
8
  import { applyPeriod } from './date-periods.js';
9
9
  import { DEFAULT_PROFILE, InvalidProfileNameError } from './profile-name.js';
10
- import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, orderListColumns, orderDetailColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, financialYearListColumns, financialYearDetailColumns, lockedPeriodDetailColumns, contractListColumns, contractDetailColumns, recurringListColumns, recurringDetailColumns, invoiceRequestListColumns, invoiceRequestDetailColumns, topCustomerColumns, monthlyRevenueColumns, voucherAttachmentColumns, employeeListColumns, employeeDetailColumns, salaryTransactionListColumns, salaryTransactionDetailColumns, attendanceTransactionListColumns, attendanceTransactionDetailColumns, absenceTransactionListColumns, absenceTransactionDetailColumns, scheduleTimeDetailColumns, } from './views.js';
10
+ import { VERSION } from './version.js';
11
+ import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, invoiceAttachmentColumns, invoiceAttachmentListColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, orderListColumns, orderDetailColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, financialYearListColumns, financialYearDetailColumns, lockedPeriodDetailColumns, contractListColumns, contractDetailColumns, recurringListColumns, recurringDetailColumns, invoiceRequestListColumns, invoiceRequestDetailColumns, topCustomerColumns, monthlyRevenueColumns, voucherAttachmentColumns, employeeListColumns, employeeDetailColumns, salaryTransactionListColumns, salaryTransactionDetailColumns, attendanceTransactionListColumns, attendanceTransactionDetailColumns, absenceTransactionListColumns, absenceTransactionDetailColumns, scheduleTimeDetailColumns, } from './views.js';
11
12
  const program = new Command();
12
13
  // A closed downstream pipe (`noxctl ... | head`, or a reader that exits early)
13
14
  // makes stdout emit EPIPE. That is normal for a CLI, and without this listener
@@ -22,7 +23,7 @@ process.stdout.on('error', (err) => {
22
23
  program
23
24
  .name('noxctl')
24
25
  .description('CLI and MCP server for Fortnox accounting')
25
- .version('0.7.4')
26
+ .version(VERSION)
26
27
  .addOption(new Option('-o, --output <format>', 'Output format (default: table on TTY, json when piped)')
27
28
  .choices(['json', 'table'])
28
29
  .default(undefined))
@@ -105,7 +106,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
105
106
  }
106
107
  throw err;
107
108
  }
108
- setResolvedProfile(resolvedProfileInfo.name);
109
+ setResolvedProfile(resolvedProfileInfo.name, resolvedProfileInfo.source);
109
110
  // Banner ownership: MCP `serve` prints its own banner in bindStartupProfile
110
111
  // so host logs (Claude Desktop) always see it. Suppress here to avoid a
111
112
  // duplicate on TTY invocations like `noxctl --profile X serve`.
@@ -198,15 +199,23 @@ program
198
199
  .option('--profile <name>', 'Profile to create/re-auth (defaults to resolved profile or "default")')
199
200
  .option('--with-salary', 'Also request the Lön (salary/payroll) scope — requires the Lön permission enabled on your Fortnox app')
200
201
  .option('--with-orders', 'Also request the offer/order scopes — requires the Order licence on the Fortnox company')
202
+ .option('--with-archive', 'Also request the archive (Arkivplats) scope — lets noxctl read files from the Fortnox file archive, e.g. attachments made via the Fortnox web UI')
201
203
  .action(async (initOpts) => {
202
- const { loadCredentials, runOAuthSetup, SCOPES, SALARY_SCOPE, ORDER_SCOPES } = await import('./auth.js');
204
+ const { inspectCredentials, runOAuthSetup, SCOPES, SALARY_SCOPE, ORDER_SCOPES, ARCHIVE_SCOPE, } = await import('./auth.js');
203
205
  const { validateProfileName } = await import('./profile-name.js');
204
- // Opt-in scopes: flag for TTY, env var for non-interactive/CI runs. Both are
205
- // licence-gated in Fortnox (Lön / Order), and asking for a scope the company
206
- // is not licensed for fails the whole authorization — hence not defaults.
206
+ // Opt-in scopes: flag for TTY, env var for non-interactive/CI runs. All are
207
+ // licence-gated in Fortnox (Lön / Order / Arkivplats), and asking for a scope
208
+ // the company is not licensed for fails the whole authorization — hence not
209
+ // defaults.
207
210
  const withSalary = Boolean(initOpts.withSalary) || process.env.FORTNOX_WITH_SALARY === '1';
208
211
  const withOrders = Boolean(initOpts.withOrders) || process.env.FORTNOX_WITH_ORDERS === '1';
209
- const scopes = [SCOPES, withOrders ? ORDER_SCOPES : '', withSalary ? SALARY_SCOPE : '']
212
+ const withArchive = Boolean(initOpts.withArchive) || process.env.FORTNOX_WITH_ARCHIVE === '1';
213
+ const scopes = [
214
+ SCOPES,
215
+ withOrders ? ORDER_SCOPES : '',
216
+ withSalary ? SALARY_SCOPE : '',
217
+ withArchive ? ARCHIVE_SCOPE : '',
218
+ ]
210
219
  .filter(Boolean)
211
220
  .join(' ');
212
221
  let targetProfile;
@@ -222,11 +231,15 @@ program
222
231
  // (verification via getCompanyInfo, runOAuthSetup's internal saveCredentials
223
232
  // call chain) targets the profile being initialized — not a stale pointer.
224
233
  if (targetProfile.toLowerCase() !== resolvedProfileInfo.name.toLowerCase()) {
225
- setResolvedProfile(targetProfile);
234
+ setResolvedProfile(targetProfile, 'flag');
226
235
  resolvedProfileInfo = { name: targetProfile, source: 'flag' };
227
236
  }
228
237
  // Step 1: Check if already configured
229
- const existing = await loadCredentials(targetProfile);
238
+ const inspection = await inspectCredentials(targetProfile);
239
+ if (inspection.state === 'locked' || inspection.state === 'inaccessible') {
240
+ fail(inspection.detail);
241
+ }
242
+ const existing = inspection.credentials;
230
243
  if (existing) {
231
244
  console.log('Existing credentials found.');
232
245
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -556,9 +569,12 @@ profile
556
569
  console.error(err instanceof Error ? err.message : String(err));
557
570
  process.exit(2);
558
571
  }
559
- const { loadCredentials } = await import('./auth.js');
560
- const creds = await loadCredentials(validated);
561
- if (!creds) {
572
+ const { inspectCredentials } = await import('./auth.js');
573
+ const inspection = await inspectCredentials(validated);
574
+ if (inspection.state === 'locked' || inspection.state === 'inaccessible') {
575
+ fail(inspection.detail);
576
+ }
577
+ if (inspection.state === 'missing') {
562
578
  console.error(`No credentials found for profile "${validated}". Run \`noxctl init --profile ${validated}\` first.`);
563
579
  process.exit(1);
564
580
  }
@@ -606,7 +622,7 @@ program
606
622
  .command('doctor')
607
623
  .description('Check setup: credentials, token, API connectivity, and scopes')
608
624
  .action(async () => {
609
- const { loadCredentials } = await import('./auth.js');
625
+ const { inspectCredentials } = await import('./auth.js');
610
626
  let ok = true;
611
627
  function pass(label, detail) {
612
628
  console.log(` ✓ ${label}${detail ? ` — ${detail}` : ''}`);
@@ -641,6 +657,9 @@ program
641
657
  if (state === 'locked') {
642
658
  fail('Dedicated keychain', 'locked — run `noxctl keychain unlock` (tap your YubiKey)');
643
659
  }
660
+ else if (state === 'missing') {
661
+ fail('Dedicated keychain', `configured but inaccessible at ${activePath} — inspect it from an unsandboxed terminal`);
662
+ }
644
663
  else {
645
664
  pass('Dedicated keychain', `active, ${state} (${activePath})`);
646
665
  }
@@ -682,27 +701,21 @@ program
682
701
  catch {
683
702
  // best-effort diagnostic — don't block doctor on filesystem issues
684
703
  }
685
- // 3. Credentials exist
686
- let creds;
687
- try {
688
- creds = await loadCredentials();
689
- }
690
- catch (err) {
691
- const { KeychainLockedError } = await import('./keychain-target.js');
692
- if (err instanceof KeychainLockedError) {
693
- fail('Credentials', err.message);
694
- console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
695
- return;
696
- }
697
- throw err;
704
+ // 3. Credential state
705
+ const inspection = await inspectCredentials();
706
+ if (inspection.state === 'locked' || inspection.state === 'inaccessible') {
707
+ fail('Credentials', `${inspection.state} ${inspection.detail}`);
708
+ console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
709
+ return;
698
710
  }
699
- if (!creds) {
700
- fail('Credentials', `not found for profile "${resolvedProfileInfo.name}" — run \`noxctl init${resolvedProfileInfo.name === DEFAULT_PROFILE
711
+ if (inspection.state === 'missing') {
712
+ fail('Credentials', `missing — not found for profile "${resolvedProfileInfo.name}" — run \`noxctl init${resolvedProfileInfo.name === DEFAULT_PROFILE
701
713
  ? ''
702
714
  : ` --profile ${resolvedProfileInfo.name}`}\` to set up`);
703
715
  console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
704
716
  return;
705
717
  }
718
+ const creds = inspection.credentials;
706
719
  pass('Credentials', 'found');
707
720
  if (creds.company_name) {
708
721
  pass('Company (cached)', creds.company_name);
@@ -795,6 +808,12 @@ keychain
795
808
  const onDarwin = process.platform === 'darwin';
796
809
  console.log('Keychain status\n');
797
810
  console.log(` Platform ${process.platform}${onDarwin ? '' : ' (dedicated keychain is macOS-only)'}`);
811
+ const { inspectCredentials } = await import('./auth.js');
812
+ const inspection = await inspectCredentials();
813
+ console.log(` Credential state ${inspection.state} — profile "${inspection.profile}" (source: ${inspection.source})`);
814
+ if (inspection.state === 'locked' || inspection.state === 'inaccessible') {
815
+ console.log(`\n ${inspection.detail}`);
816
+ }
798
817
  if (!onDarwin)
799
818
  return;
800
819
  const activePath = kt.activeKeychainPath();
@@ -822,10 +841,13 @@ keychain
822
841
  .action(async () => {
823
842
  requireDarwin();
824
843
  const kt = await import('./keychain-target.js');
825
- const kcPath = kt.activeKeychainPath() ?? kt.dedicatedKeychainPath();
844
+ const activePath = kt.activeKeychainPath();
845
+ const kcPath = activePath ?? kt.dedicatedKeychainPath();
826
846
  const state = kt.keychainLockState(kcPath);
827
847
  if (state === 'missing') {
828
- console.error('No dedicated keychain found. Run `noxctl keychain init` first.');
848
+ console.error(activePath
849
+ ? `The configured keychain at ${kcPath} is inaccessible. Retry from an unsandboxed terminal; do not re-run noxctl keychain init.`
850
+ : 'No dedicated keychain found. Run `noxctl keychain init` first.');
829
851
  process.exit(1);
830
852
  }
831
853
  if (state === 'unlocked') {
@@ -868,9 +890,12 @@ keychain
868
890
  .action(async () => {
869
891
  requireDarwin();
870
892
  const kt = await import('./keychain-target.js');
871
- const kcPath = kt.activeKeychainPath() ?? kt.dedicatedKeychainPath();
893
+ const activePath = kt.activeKeychainPath();
894
+ const kcPath = activePath ?? kt.dedicatedKeychainPath();
872
895
  if (kt.keychainLockState(kcPath) === 'missing') {
873
- console.error('No dedicated keychain found.');
896
+ console.error(activePath
897
+ ? `The configured keychain at ${kcPath} is inaccessible. Retry from an unsandboxed terminal.`
898
+ : 'No dedicated keychain found.');
874
899
  process.exit(1);
875
900
  }
876
901
  kt.lockKeychain(kcPath);
@@ -1263,6 +1288,47 @@ invoices
1263
1288
  const data = await creditInvoice(documentNumber);
1264
1289
  outputConfirmation(`Invoice ${documentNumber} credited.`, json(), data, invoiceConfirmColumns, 'Invoice');
1265
1290
  });
1291
+ invoices
1292
+ .command('attach <documentNumber> <files...>')
1293
+ .description('Upload receipt/underlag files and attach them to a customer invoice')
1294
+ .option('--no-include-on-send', 'Do not bundle the file when the invoice is sent (default: bundled)')
1295
+ .option('-y, --yes', 'Skip confirmation prompt')
1296
+ .option('--dry-run', 'Preview without uploading')
1297
+ .addHelpText('after', `
1298
+ Requires the "archive" scope — run \`noxctl init --with-archive\` first if
1299
+ \`noxctl doctor\` reports it missing.
1300
+
1301
+ Examples:
1302
+ noxctl invoices attach 91110646 underlag.pdf
1303
+ noxctl invoices attach 91110646 underlag.pdf --no-include-on-send`)
1304
+ .action(async (documentNumber, files, opts) => {
1305
+ const { attachInvoiceFiles } = await import('./operations/invoices.js');
1306
+ if (!(await confirmMutation(`Attach ${files.length} file(s) to invoice ${documentNumber}`, opts, {
1307
+ files,
1308
+ includeOnSend: opts.includeOnSend ?? true,
1309
+ }))) {
1310
+ return;
1311
+ }
1312
+ const results = await attachInvoiceFiles({
1313
+ documentNumber,
1314
+ filePaths: files,
1315
+ includeOnSend: opts.includeOnSend,
1316
+ });
1317
+ if (json()) {
1318
+ console.log(JSON.stringify({ Attachments: results }, null, 2));
1319
+ }
1320
+ else {
1321
+ outputList(results, invoiceAttachmentColumns, false, results);
1322
+ }
1323
+ });
1324
+ invoices
1325
+ .command('attachments <documentNumber>')
1326
+ .description('List files attached to a customer invoice')
1327
+ .action(async (documentNumber) => {
1328
+ const { listInvoiceAttachments } = await import('./operations/invoices.js');
1329
+ const results = await listInvoiceAttachments(documentNumber);
1330
+ outputList(results, invoiceAttachmentListColumns, json(), { Attachments: results });
1331
+ });
1266
1332
  // --- tax ---
1267
1333
  const tax = program.command('tax').description('Tax operations');
1268
1334
  tax
@@ -1748,6 +1814,51 @@ Examples:
1748
1814
  outputList(results, voucherAttachmentColumns, false, results);
1749
1815
  }
1750
1816
  });
1817
+ vouchers
1818
+ .command('attachments <series> <voucherNumber>')
1819
+ .description('List files (receipts/underlag) attached to a voucher')
1820
+ .option('--year <number>', 'Financial year (recommended — voucher numbers reset between financial years)', parseInt)
1821
+ .action(async (series, voucherNumber, opts) => {
1822
+ const { listVoucherAttachments } = await import('./operations/vouchers.js');
1823
+ const attachments = await listVoucherAttachments(series, voucherNumber, opts.year);
1824
+ if (json()) {
1825
+ console.log(JSON.stringify({ Attachments: attachments }, null, 2));
1826
+ }
1827
+ else {
1828
+ outputList(attachments, voucherAttachmentColumns, false, attachments);
1829
+ }
1830
+ });
1831
+ vouchers
1832
+ .command('file <fileId>')
1833
+ .description('Download a file attached to a voucher (get the fileId from "vouchers attachments")')
1834
+ .option('-f, --file <path>', 'Write the file here (- for stdout; default: voucher-file-<fileId><ext>)')
1835
+ .addHelpText('after', `
1836
+ Examples:
1837
+ noxctl vouchers attachments A 60 --year 4
1838
+ noxctl vouchers file c7e578d8-59a7-4e00-b29b-b99e4d9ede63
1839
+ noxctl vouchers file c7e578d8-59a7-4e00-b29b-b99e4d9ede63 --file receipt.pdf`)
1840
+ .action(async (fileId, opts) => {
1841
+ const { getVoucherFile, extensionForMime } = await import('./operations/vouchers.js');
1842
+ const toStdout = opts.file === '-';
1843
+ if (toStdout && program.opts().output === 'json') {
1844
+ throw new Error('--file - writes raw file bytes to stdout and cannot be combined with --output json.');
1845
+ }
1846
+ const file = await getVoucherFile(fileId);
1847
+ if (toStdout) {
1848
+ await new Promise((resolve, reject) => {
1849
+ process.stdout.write(file.buffer, (err) => (err ? reject(err) : resolve()));
1850
+ });
1851
+ return;
1852
+ }
1853
+ const path = opts.file ?? `voucher-file-${fileId}${extensionForMime(file.contentType)}`;
1854
+ writeFileSync(path, file.buffer);
1855
+ outputConfirmation(`File saved to ${path} (${file.contentType}, ${file.buffer.length} bytes).`, json(), {
1856
+ FileId: fileId,
1857
+ Path: path,
1858
+ ContentType: file.contentType,
1859
+ Bytes: file.buffer.length,
1860
+ });
1861
+ });
1751
1862
  // --- invoice-payments ---
1752
1863
  const invoicePayments = program
1753
1864
  .command('invoice-payments')