@substrat-run/connector-fortnox 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.
- package/README.md +204 -0
- package/dist/aggregate.d.ts +69 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +101 -0
- package/dist/aggregate.js.map +1 -0
- package/dist/api.d.ts +154 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +276 -0
- package/dist/api.js.map +1 -0
- package/dist/index.d.ts +260 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +477 -0
- package/dist/index.js.map +1 -0
- package/dist/mock.d.ts +59 -0
- package/dist/mock.d.ts.map +1 -0
- package/dist/mock.js +111 -0
- package/dist/mock.js.map +1 -0
- package/dist/sie4.d.ts +132 -0
- package/dist/sie4.d.ts.map +1 -0
- package/dist/sie4.js +265 -0
- package/dist/sie4.js.map +1 -0
- package/package.json +45 -0
package/dist/api.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A thin, typed client over the Fortnox REST API and its OAuth2 token endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Every call goes through the connection's `fetch`, never a global one: that is what
|
|
6
|
+
* gets it a timeout, an egress policy, and health recorded against the right
|
|
7
|
+
* connection. Module code cannot reach any of this — boundary-lint bans `fetch`
|
|
8
|
+
* outright — and a connector is host code.
|
|
9
|
+
*
|
|
10
|
+
* ## The two hosts are different, and mixing them is the first mistake
|
|
11
|
+
*
|
|
12
|
+
* OAuth lives at `apps.fortnox.se/oauth-v1`; the API lives at `api.fortnox.se/3`.
|
|
13
|
+
* They are separate origins with separate paths, so both are named here and neither
|
|
14
|
+
* is derived from the other.
|
|
15
|
+
*/
|
|
16
|
+
/** The OAuth2 host — `/auth` (consent) and `/token` (minting) hang off this. */
|
|
17
|
+
export const FORTNOX_OAUTH_BASE = 'https://apps.fortnox.se/oauth-v1';
|
|
18
|
+
/** The REST host — every data read hangs off this. */
|
|
19
|
+
export const FORTNOX_API_BASE = 'https://api.fortnox.se/3';
|
|
20
|
+
/**
|
|
21
|
+
* A Fortnox connection's credential — the CLIENT-CREDENTIALS triple.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately NOT the authorization-code flow's tokens. Fortnox supports
|
|
24
|
+
* `grant_type=client_credentials` for a *service* consent, and that changes what a
|
|
25
|
+
* connection has to store: an access token is minted on demand from these three
|
|
26
|
+
* static values and lives one hour, so there is no refresh token — and therefore
|
|
27
|
+
* none of the rotation hazard the authorization-code flow carries. Fortnox's refresh
|
|
28
|
+
* tokens are single-use and rotating: two concurrent refreshes kill the connection,
|
|
29
|
+
* one winning and the other saving an already-dead token. A connector that mints
|
|
30
|
+
* from static credentials cannot reach that state at all.
|
|
31
|
+
*
|
|
32
|
+
* The price is a one-time consent round per company, in a browser, with
|
|
33
|
+
* `account_type=service` — see {@link fortnoxConsentUrl}. What that round yields is
|
|
34
|
+
* the `tenantId` below; the client pair comes from the Developer Portal and is the
|
|
35
|
+
* same for every company this integration serves.
|
|
36
|
+
*/
|
|
37
|
+
export const fortnoxSecret = z.object({
|
|
38
|
+
/** From the Fortnox Developer Portal — the integration's identity. */
|
|
39
|
+
clientId: z.string().min(1),
|
|
40
|
+
/** From the Developer Portal — write-only, sealed by the host's SecretBox. */
|
|
41
|
+
clientSecret: z.string().min(1),
|
|
42
|
+
/**
|
|
43
|
+
* WHICH Fortnox company this connection reads, as the `TenantId` header.
|
|
44
|
+
*
|
|
45
|
+
* Numeric, and it is the company's `DatabaseNumber` — not a name, not the org
|
|
46
|
+
* number, and not anything a person would recognise. It is the one part of this
|
|
47
|
+
* credential that differs per company, which is what makes one client pair serve a
|
|
48
|
+
* whole fleet. Held as a string because it is a header value, never arithmetic.
|
|
49
|
+
*/
|
|
50
|
+
tenantId: z.string().regex(/^\d+$/, 'Fortnox TenantId is the numeric DatabaseNumber'),
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* The consent URL a company's sysadmin visits ONCE (the PDF's step 1, with the one
|
|
54
|
+
* parameter that changes everything).
|
|
55
|
+
*
|
|
56
|
+
* `account_type=service` is what makes the resulting consent mintable by
|
|
57
|
+
* `client_credentials` afterwards. Without it the consent is bound to the person who
|
|
58
|
+
* granted it, and this connector's whole credential model does not apply — you are
|
|
59
|
+
* back to storing and rotating refresh tokens per company.
|
|
60
|
+
*
|
|
61
|
+
* `access_type=offline` is kept because the callback still exchanges a code once, to
|
|
62
|
+
* discover the `DatabaseNumber` this connection will be keyed by
|
|
63
|
+
* ({@link FortnoxApi.companyInformation}). After that the code path is never used
|
|
64
|
+
* again.
|
|
65
|
+
*
|
|
66
|
+
* **Scopes cannot be widened later without a new consent round.** Ask for what the
|
|
67
|
+
* integration will need, not what it needs today — this is the single most expensive
|
|
68
|
+
* thing to get wrong here, because fixing it means going back to every customer.
|
|
69
|
+
*/
|
|
70
|
+
export function fortnoxConsentUrl(input) {
|
|
71
|
+
const url = new URL(`${input.oauthBase ?? FORTNOX_OAUTH_BASE}/auth`);
|
|
72
|
+
url.searchParams.set('client_id', input.clientId);
|
|
73
|
+
url.searchParams.set('redirect_uri', input.redirectUri);
|
|
74
|
+
url.searchParams.set('scope', input.scopes.join(' '));
|
|
75
|
+
url.searchParams.set('state', input.state);
|
|
76
|
+
url.searchParams.set('response_type', 'code');
|
|
77
|
+
url.searchParams.set('access_type', 'offline');
|
|
78
|
+
url.searchParams.set('account_type', 'service');
|
|
79
|
+
return url.toString();
|
|
80
|
+
}
|
|
81
|
+
/** One financial year, as `GET /3/financialyears` returns it. Extra fields ignored. */
|
|
82
|
+
export const fortnoxFinancialYear = z.object({
|
|
83
|
+
Id: z.number(),
|
|
84
|
+
FromDate: z.string().min(1),
|
|
85
|
+
ToDate: z.string().min(1),
|
|
86
|
+
AccountingMethod: z.string().optional(),
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* The company, as `GET /3/companyinformation` returns it.
|
|
90
|
+
*
|
|
91
|
+
* `DatabaseNumber` is the whole reason this endpoint is called: it IS the `TenantId`
|
|
92
|
+
* header value, and it is how a connect flow learns which company the consent it just
|
|
93
|
+
* received belongs to. It is also the cheapest authenticated read Fortnox offers, so
|
|
94
|
+
* it doubles as the credential probe.
|
|
95
|
+
*/
|
|
96
|
+
export const fortnoxCompany = z.object({
|
|
97
|
+
CompanyName: z.string().default(''),
|
|
98
|
+
OrganizationNumber: z.string().default(''),
|
|
99
|
+
DatabaseNumber: z.union([z.number(), z.string()]).optional(),
|
|
100
|
+
CountryCode: z.string().optional(),
|
|
101
|
+
});
|
|
102
|
+
const tokenResponse = z.object({
|
|
103
|
+
access_token: z.string().min(1),
|
|
104
|
+
token_type: z.string().default('bearer'),
|
|
105
|
+
expires_in: z.number().default(3600),
|
|
106
|
+
scope: z.string().default(''),
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* A Fortnox API failure, with the one bit a caller actually branches on.
|
|
110
|
+
*
|
|
111
|
+
* `refused` means the provider said "not with these credentials" — a 401 or 403.
|
|
112
|
+
* Everything else (a timeout, a 5xx, a parse failure) says nothing about the
|
|
113
|
+
* credential, and treating it as a refusal would make a Fortnox outage look like
|
|
114
|
+
* every tenant's keys going bad at once.
|
|
115
|
+
*/
|
|
116
|
+
export class FortnoxApiError extends Error {
|
|
117
|
+
status;
|
|
118
|
+
refused;
|
|
119
|
+
body;
|
|
120
|
+
constructor(message, status, body) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.name = 'FortnoxApiError';
|
|
123
|
+
this.status = status;
|
|
124
|
+
this.refused = status === 401 || status === 403;
|
|
125
|
+
this.body = body;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Fortnox's error envelope. Two shapes in the wild, and a caller wants the message
|
|
130
|
+
* from whichever arrived rather than a bare status.
|
|
131
|
+
*/
|
|
132
|
+
function errorMessage(body, status) {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(body);
|
|
135
|
+
const e = parsed;
|
|
136
|
+
const found = e.ErrorInformation?.message ??
|
|
137
|
+
e.ErrorInformation?.Message ??
|
|
138
|
+
e.error_description ??
|
|
139
|
+
e.message ??
|
|
140
|
+
(typeof e.error === 'string' ? e.error : undefined);
|
|
141
|
+
if (found)
|
|
142
|
+
return found;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Not JSON — fall through to the raw slice, which is more use than nothing.
|
|
146
|
+
}
|
|
147
|
+
const slice = body.trim().slice(0, 200);
|
|
148
|
+
return slice === '' ? `HTTP ${status}` : slice;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* `JSON.parse`, but a non-JSON body stays inside this module's error contract.
|
|
152
|
+
*
|
|
153
|
+
* Both parse sites run AFTER `res.ok`, which is exactly when this bites: a proxy,
|
|
154
|
+
* captive portal or gateway that answers `200` with an HTML page makes a bare
|
|
155
|
+
* `JSON.parse` throw a `SyntaxError`, and the caller loses `status`, `body` and
|
|
156
|
+
* `refused` — the three fields {@link FortnoxApiError} exists to carry, and the ones a
|
|
157
|
+
* sweep reports and a probe branches on. A parse failure is a fact about the response,
|
|
158
|
+
* so it is reported as one.
|
|
159
|
+
*/
|
|
160
|
+
function asJson(body, what) {
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(body);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new FortnoxApiError(`${what} was not JSON`, 502, body);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export class FortnoxApi {
|
|
169
|
+
conn;
|
|
170
|
+
apiBase;
|
|
171
|
+
oauthBase;
|
|
172
|
+
/**
|
|
173
|
+
* The minted token and when it dies, cached for the life of THIS instance.
|
|
174
|
+
*
|
|
175
|
+
* Per-instance rather than per-connection-global on purpose: an instance is built
|
|
176
|
+
* for one sweep pass, so the cache lives exactly as long as the work that uses it
|
|
177
|
+
* and no cross-request state accumulates in a Worker's isolate. A token is an hour
|
|
178
|
+
* long and a pass is seconds, so one mint serves a whole pass.
|
|
179
|
+
*/
|
|
180
|
+
token = null;
|
|
181
|
+
constructor(conn, options) {
|
|
182
|
+
this.conn = conn;
|
|
183
|
+
this.apiBase = options?.apiBase ?? FORTNOX_API_BASE;
|
|
184
|
+
this.oauthBase = options?.oauthBase ?? FORTNOX_OAUTH_BASE;
|
|
185
|
+
this.now = options?.now ?? (() => Date.now());
|
|
186
|
+
}
|
|
187
|
+
now;
|
|
188
|
+
secret() {
|
|
189
|
+
const parsed = fortnoxSecret.safeParse(this.conn.secret);
|
|
190
|
+
if (!parsed.success) {
|
|
191
|
+
throw new FortnoxApiError(`incomplete Fortnox credential: ${parsed.error.issues.map((i) => i.path.join('.')).join(', ')}`, 400, '');
|
|
192
|
+
}
|
|
193
|
+
return parsed.data;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Mint (or reuse) an access token via `grant_type=client_credentials`.
|
|
197
|
+
*
|
|
198
|
+
* The 60-second skew is the PDF's, and it is right for a different reason here:
|
|
199
|
+
* there is no refresh token to lose, so an early re-mint costs one extra round trip
|
|
200
|
+
* rather than risking a rotation race. Cheap insurance against a token expiring
|
|
201
|
+
* mid-pass.
|
|
202
|
+
*/
|
|
203
|
+
async accessToken() {
|
|
204
|
+
if (this.token && this.token.expiresAtMs - this.now() > 60_000)
|
|
205
|
+
return this.token.value;
|
|
206
|
+
const { clientId, clientSecret, tenantId } = this.secret();
|
|
207
|
+
const res = await this.conn.fetch(`${this.oauthBase}/token`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
|
|
211
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
212
|
+
// The company. Without it Fortnox cannot tell which consent to mint against,
|
|
213
|
+
// and answers 400 — which reads like a bad client pair and is not.
|
|
214
|
+
TenantId: tenantId,
|
|
215
|
+
},
|
|
216
|
+
body: 'grant_type=client_credentials',
|
|
217
|
+
});
|
|
218
|
+
const body = await res.text();
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
throw new FortnoxApiError(`Fortnox token mint failed: ${errorMessage(body, res.status)}`, res.status, body);
|
|
221
|
+
}
|
|
222
|
+
const parsed = tokenResponse.safeParse(asJson(body, 'Fortnox token response'));
|
|
223
|
+
if (!parsed.success) {
|
|
224
|
+
throw new FortnoxApiError('Fortnox token response was not the documented shape', 502, body);
|
|
225
|
+
}
|
|
226
|
+
this.token = {
|
|
227
|
+
value: parsed.data.access_token,
|
|
228
|
+
expiresAtMs: this.now() + parsed.data.expires_in * 1000,
|
|
229
|
+
};
|
|
230
|
+
return this.token.value;
|
|
231
|
+
}
|
|
232
|
+
/** A JSON GET against the REST host, authenticated with a freshly-ensured token. */
|
|
233
|
+
async getJson(path) {
|
|
234
|
+
const token = await this.accessToken();
|
|
235
|
+
const res = await this.conn.fetch(`${this.apiBase}${path}`, {
|
|
236
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
237
|
+
});
|
|
238
|
+
const body = await res.text();
|
|
239
|
+
if (!res.ok) {
|
|
240
|
+
throw new FortnoxApiError(`Fortnox GET ${path} failed: ${errorMessage(body, res.status)}`, res.status, body);
|
|
241
|
+
}
|
|
242
|
+
return asJson(body, `Fortnox GET ${path}`);
|
|
243
|
+
}
|
|
244
|
+
/** The company — the probe read, and where a connect flow learns `DatabaseNumber`. */
|
|
245
|
+
async companyInformation() {
|
|
246
|
+
const raw = (await this.getJson('/companyinformation'));
|
|
247
|
+
return fortnoxCompany.parse(raw.CompanyInformation ?? raw);
|
|
248
|
+
}
|
|
249
|
+
/** Every financial year the company has, newest first is NOT guaranteed — sort yourself. */
|
|
250
|
+
async financialYears() {
|
|
251
|
+
const raw = (await this.getJson('/financialyears'));
|
|
252
|
+
return z.array(fortnoxFinancialYear).parse(raw.FinancialYears ?? []);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The whole year's bookkeeping as one SIE4 file — the read this connector exists for.
|
|
256
|
+
*
|
|
257
|
+
* **The response is ISO-8859-1, not UTF-8**, and nothing in the response says so.
|
|
258
|
+
* Decoding it as UTF-8 does not throw; it silently mangles every å/ä/ö in every
|
|
259
|
+
* account name, cost-centre label and verification text — which is a corrupted
|
|
260
|
+
* ledger that looks like a working one. So the bytes are taken as an ArrayBuffer and
|
|
261
|
+
* decoded explicitly, and this is the only place in the connector that knows.
|
|
262
|
+
*/
|
|
263
|
+
async sieFile(financialYearId) {
|
|
264
|
+
const token = await this.accessToken();
|
|
265
|
+
const path = `/sie/4?financialyear=${financialYearId}`;
|
|
266
|
+
const res = await this.conn.fetch(`${this.apiBase}${path}`, {
|
|
267
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
268
|
+
});
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
const body = await res.text();
|
|
271
|
+
throw new FortnoxApiError(`Fortnox SIE4 export failed: ${errorMessage(body, res.status)}`, res.status, body);
|
|
272
|
+
}
|
|
273
|
+
return new TextDecoder('iso-8859-1').decode(await res.arrayBuffer());
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ConnectionActivity, type ConnectionProbe, type ConnectionId } from '@substrat-run/contracts';
|
|
3
|
+
import type { FetchLike, ScopeHost } from '@substrat-run/kernel';
|
|
4
|
+
export { FortnoxApi, FortnoxApiError, FORTNOX_API_BASE, FORTNOX_OAUTH_BASE, fortnoxConsentUrl, fortnoxSecret, type FortnoxCompany, type FortnoxFinancialYear, type FortnoxSecret, } from './api.js';
|
|
5
|
+
export { parseSie4, splitSieLine, sieAmount, sieDate, type SieAccount, type SieDimension, type SieLedger, type SieObject, type SieTransaction, type SieVoucher, } from './sie4.js';
|
|
6
|
+
export { COST_CENTRE_DIMENSION, financialYearFor, summarizeLedger, type LedgerBalance, type LedgerSummary, } from './aggregate.js';
|
|
7
|
+
export { FortnoxMock, type FortnoxMockOptions } from './mock.js';
|
|
8
|
+
/**
|
|
9
|
+
* The Fortnox connector — the INBOUND half of accounting integration.
|
|
10
|
+
*
|
|
11
|
+
* ## Poll-only, and that is a design fact rather than an omission
|
|
12
|
+
*
|
|
13
|
+
* `connector-scrive` has two halves: a dispatch handler driven by an engine event, and
|
|
14
|
+
* a sweep that polls the provider. This connector has **only the sweep**, and registers
|
|
15
|
+
* no event handler at all, because nothing inside a scope initiates the work. A
|
|
16
|
+
* vertical does not *ask* for last month's bookkeeping the way it asks for a signature;
|
|
17
|
+
* the bookkeeping simply changes at Fortnox, and the platform finds out by looking.
|
|
18
|
+
*
|
|
19
|
+
* So there is no `registerFortnoxConnector`. `sweepFortnoxLedger` is a
|
|
20
|
+
* {@link ConnectorSweeper}, the deployment binds it into the platform sweeper exactly
|
|
21
|
+
* as it binds Scrive's, and that is the whole trigger surface.
|
|
22
|
+
*
|
|
23
|
+
* ## Where the data lands, and why the connector does not decide
|
|
24
|
+
*
|
|
25
|
+
* A sweep has no delivered event, so it has neither a scope to write to nor authority
|
|
26
|
+
* to write with. Scrive gets both from its dispatch ledger — the row it wrote when the
|
|
27
|
+
* event arrived. This connector has no such moment, so the scope is declared **once**,
|
|
28
|
+
* explicitly, by {@link bindFortnoxScope}: which scope, which operation to land the
|
|
29
|
+
* ledger through, and which permission that operation checks.
|
|
30
|
+
*
|
|
31
|
+
* The operation is the *consumer's*, not this connector's, and that is deliberate. What
|
|
32
|
+
* comes out of Fortnox is neutral accounting fact — accounts, cost centres, months,
|
|
33
|
+
* debit-positive sums. What a business *means* by them (which account is
|
|
34
|
+
* `lokal_grundhyra`, which sign normalizes it, which group it rolls into) is vocabulary,
|
|
35
|
+
* and vocabulary is the vertical's layer. A connector that mapped accounts to row keys
|
|
36
|
+
* would be a vertical wearing a connector's clothes, and the second customer with a
|
|
37
|
+
* different chart of accounts would have to fork it.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* The standing grants this connector requires — deliberately EMPTY, with a mechanism
|
|
41
|
+
* in place of a declaration.
|
|
42
|
+
*
|
|
43
|
+
* Every other connector names its return-path permissions here so `lint:connector-grants`
|
|
44
|
+
* can prove the dashboard's door is able to grant them. This one genuinely cannot: the
|
|
45
|
+
* permission it needs is whatever the *consumer's* landing operation checks, which
|
|
46
|
+
* differs per vertical and is unknown at this package's build time.
|
|
47
|
+
*
|
|
48
|
+
* That would be an invisible hole — the exact shape of #841, where a connector needed a
|
|
49
|
+
* grant no door could give and the way it surfaced was a failure months later. So the
|
|
50
|
+
* check moves from build time to bind time: {@link bindFortnoxScope} verifies the
|
|
51
|
+
* connection actually holds the named permission in the named scope and **refuses the
|
|
52
|
+
* binding otherwise**, naming what is missing. A sweep can therefore never be configured
|
|
53
|
+
* into a state where it fetches a year of bookkeeping and cannot write it down.
|
|
54
|
+
*/
|
|
55
|
+
export declare const FORTNOX_CONNECTION_GRANTS: readonly [];
|
|
56
|
+
/**
|
|
57
|
+
* What the connector remembers about one scope it syncs into.
|
|
58
|
+
*
|
|
59
|
+
* Directory-side (`putConnectorState`) for the same reason Scrive's ledger is: this is
|
|
60
|
+
* a connector's own bookkeeping, it must survive across sweeps, and it must be readable
|
|
61
|
+
* without entering a scope.
|
|
62
|
+
*/
|
|
63
|
+
export interface FortnoxBinding {
|
|
64
|
+
scopeId: string;
|
|
65
|
+
tenantId: string;
|
|
66
|
+
/** The scope's vertical — half the key that reopens the connection to poll. */
|
|
67
|
+
vertical: string;
|
|
68
|
+
/**
|
|
69
|
+
* The operation the parsed ledger is landed through, e.g. `'ledger/record-period'`.
|
|
70
|
+
*
|
|
71
|
+
* Named by whoever binds, never defaulted. A default here would be a name this
|
|
72
|
+
* package invented for an operation it does not implement — so the first deployment
|
|
73
|
+
* to get it wrong would find out at sweep time, from an `unknown operation` error
|
|
74
|
+
* three layers down, rather than at bind time from this function.
|
|
75
|
+
*/
|
|
76
|
+
operation: string;
|
|
77
|
+
/**
|
|
78
|
+
* The permission `operation` checks, which the connection must hold on `scopeId`.
|
|
79
|
+
*
|
|
80
|
+
* Recorded so the sweep can re-verify cheaply and so an operator can read the whole
|
|
81
|
+
* authority of this binding without opening the vertical's source.
|
|
82
|
+
*/
|
|
83
|
+
permission: string;
|
|
84
|
+
boundAt: string;
|
|
85
|
+
/** Set after the first successful sync — the cursor that makes a re-sync cheap. */
|
|
86
|
+
lastSync?: {
|
|
87
|
+
financialYearId: number;
|
|
88
|
+
syncedAt: string;
|
|
89
|
+
/**
|
|
90
|
+
* SHA-256 of the SIE payload AND the window it was read through. Unchanged ⇒ the
|
|
91
|
+
* sync is skipped without landing. The window is in here because what lands is the
|
|
92
|
+
* payload filtered to it, so the same bytes read through a different period are a
|
|
93
|
+
* different result — see the note where this is computed.
|
|
94
|
+
*/
|
|
95
|
+
contentHash: string;
|
|
96
|
+
balances: number;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One page of parsed bookkeeping, as it crosses into a scope.
|
|
101
|
+
*
|
|
102
|
+
* Parsed with this schema on the way OUT, before every `invoke`. The engine-seam rule
|
|
103
|
+
* (`returns()`) exists because a value crossing a version boundary must be pinned to a
|
|
104
|
+
* published shape rather than to whatever the code currently produces, and a connector
|
|
105
|
+
* seam is the same boundary with a network in the middle: a vertical compiled against
|
|
106
|
+
* one version of this package and running against another must get a throw, never a
|
|
107
|
+
* silently-reshaped ledger on a screen.
|
|
108
|
+
*/
|
|
109
|
+
export declare const fortnoxLedgerPage: z.ZodObject<{
|
|
110
|
+
syncId: z.ZodString;
|
|
111
|
+
connectionId: z.ZodString;
|
|
112
|
+
company: z.ZodObject<{
|
|
113
|
+
name: z.ZodString;
|
|
114
|
+
organizationNumber: z.ZodString;
|
|
115
|
+
}, z.core.$strip>;
|
|
116
|
+
financialYear: z.ZodObject<{
|
|
117
|
+
id: z.ZodNumber;
|
|
118
|
+
from: z.ZodString;
|
|
119
|
+
to: z.ZodString;
|
|
120
|
+
}, z.core.$strip>;
|
|
121
|
+
page: z.ZodNumber;
|
|
122
|
+
pageCount: z.ZodNumber;
|
|
123
|
+
final: z.ZodBoolean;
|
|
124
|
+
accounts: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
125
|
+
number: z.ZodString;
|
|
126
|
+
name: z.ZodString;
|
|
127
|
+
}, z.core.$strip>>>;
|
|
128
|
+
costCentres: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
129
|
+
code: z.ZodString;
|
|
130
|
+
name: z.ZodString;
|
|
131
|
+
}, z.core.$strip>>>;
|
|
132
|
+
balances: z.ZodArray<z.ZodObject<{
|
|
133
|
+
account: z.ZodString;
|
|
134
|
+
costCentre: z.ZodNullable<z.ZodString>;
|
|
135
|
+
month: z.ZodString;
|
|
136
|
+
amount: z.ZodObject<{
|
|
137
|
+
amount: z.core.$ZodBranded<z.ZodString, "MoneyAmount", "out">;
|
|
138
|
+
currency: z.core.$ZodBranded<z.ZodString, "CurrencyCode", "out">;
|
|
139
|
+
}, z.core.$strip>;
|
|
140
|
+
}, z.core.$strip>>;
|
|
141
|
+
}, z.core.$strip>;
|
|
142
|
+
export type FortnoxLedgerPage = z.infer<typeof fortnoxLedgerPage>;
|
|
143
|
+
export interface FortnoxConnectorOptions {
|
|
144
|
+
fetch: FetchLike;
|
|
145
|
+
apiBase?: string;
|
|
146
|
+
oauthBase?: string;
|
|
147
|
+
timeoutMs?: number;
|
|
148
|
+
/** Injected so a test can assert elapsed time without sleeping. */
|
|
149
|
+
now?: () => number;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Declare that a connection should sync one scope — the one-time setup a poll-only
|
|
153
|
+
* connector needs in place of a dispatch.
|
|
154
|
+
*
|
|
155
|
+
* **Refuses a binding whose grant is missing**, which is the whole reason this is a
|
|
156
|
+
* function rather than a config object. The alternative — write the binding, discover
|
|
157
|
+
* at sweep time that the connection cannot invoke the operation — fails in the worst
|
|
158
|
+
* possible place: after a year of bookkeeping has been fetched, in a background timer
|
|
159
|
+
* nobody is watching, with a provider round trip already spent. Here it fails in the
|
|
160
|
+
* operator's hands, naming the permission to grant.
|
|
161
|
+
*/
|
|
162
|
+
export declare function bindFortnoxScope(host: ScopeHost, input: {
|
|
163
|
+
connectionId: ConnectionId;
|
|
164
|
+
tenantId: string;
|
|
165
|
+
scopeId: string;
|
|
166
|
+
vertical: string;
|
|
167
|
+
operation: string;
|
|
168
|
+
permission: string;
|
|
169
|
+
now?: () => number;
|
|
170
|
+
}): Promise<FortnoxBinding>;
|
|
171
|
+
/** Every scope this connection syncs into. */
|
|
172
|
+
export declare function listFortnoxBindings(host: ScopeHost, connectionId: ConnectionId): Promise<FortnoxBinding[]>;
|
|
173
|
+
/**
|
|
174
|
+
* Stop syncing one scope. The binding row is replaced with a tombstone rather than
|
|
175
|
+
* removed, because `putConnectorState` is the only verb this surface has — and an
|
|
176
|
+
* unbound scope that a later sweep silently re-adopts would be worse than a visible
|
|
177
|
+
* dead row.
|
|
178
|
+
*/
|
|
179
|
+
export declare function unbindFortnoxScope(host: ScopeHost, connectionId: ConnectionId, scopeId: string): Promise<void>;
|
|
180
|
+
/** What one scope's sync did. */
|
|
181
|
+
export interface FortnoxSyncResult {
|
|
182
|
+
scopeId: string;
|
|
183
|
+
/** False when the books were byte-identical to the last sync — nothing was landed. */
|
|
184
|
+
changed: boolean;
|
|
185
|
+
syncId: string;
|
|
186
|
+
financialYearId: number | null;
|
|
187
|
+
balances: number;
|
|
188
|
+
pages: number;
|
|
189
|
+
/** Transactions dropped for falling outside the requested window. */
|
|
190
|
+
outOfRange: number;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Sync ONE bound scope: fetch the year that covers `period`, parse it, sum it, and land
|
|
194
|
+
* it through the binding's operation as the connection itself (#97).
|
|
195
|
+
*
|
|
196
|
+
* Idempotent and cheap on a no-op. The SIE payload is hashed before anything is landed,
|
|
197
|
+
* and an unchanged hash returns `changed: false` without a single `invoke` — which
|
|
198
|
+
* matters because a sweep runs on a timer and most passes find books nobody has touched.
|
|
199
|
+
*/
|
|
200
|
+
export declare function syncFortnoxScope(host: ScopeHost, connectionId: ConnectionId, binding: FortnoxBinding, options: FortnoxConnectorOptions & {
|
|
201
|
+
period?: {
|
|
202
|
+
from: string;
|
|
203
|
+
to: string;
|
|
204
|
+
};
|
|
205
|
+
}): Promise<FortnoxSyncResult>;
|
|
206
|
+
/** What one sweep pass over a connection did. */
|
|
207
|
+
export interface FortnoxSweepResult {
|
|
208
|
+
found: number;
|
|
209
|
+
synced: FortnoxSyncResult[];
|
|
210
|
+
unchanged: number;
|
|
211
|
+
failed: {
|
|
212
|
+
scopeId: string;
|
|
213
|
+
error: string;
|
|
214
|
+
}[];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Poll Fortnox for every scope this connection is bound to — the {@link ConnectorSweeper}
|
|
218
|
+
* a deployment schedules.
|
|
219
|
+
*
|
|
220
|
+
* A timer calls this; it holds no timer itself. That keeps the trigger a deployment
|
|
221
|
+
* concern (`startPlatformSweeper` on node, `definePlatformSweeperDO`'s alarm on
|
|
222
|
+
* Cloudflare) and this a plain, testable function.
|
|
223
|
+
*
|
|
224
|
+
* Robust the way a poller must be: an unchanged year is skipped without landing
|
|
225
|
+
* anything, and a failure on one scope is recorded and stepped over rather than sinking
|
|
226
|
+
* the pass — one vertical's missing grant must not stop another tenant's books syncing.
|
|
227
|
+
*/
|
|
228
|
+
export declare function sweepFortnoxLedger(host: ScopeHost, connectionId: ConnectionId, options: FortnoxConnectorOptions & {
|
|
229
|
+
period?: {
|
|
230
|
+
from: string;
|
|
231
|
+
to: string;
|
|
232
|
+
};
|
|
233
|
+
}): Promise<FortnoxSweepResult>;
|
|
234
|
+
/**
|
|
235
|
+
* Probe a credential that is not stored yet — the connect-time check (#605).
|
|
236
|
+
*
|
|
237
|
+
* Takes the candidate secret directly, touches no connection and no store, and records
|
|
238
|
+
* no health: there may be no connection to record against, and a candidate's failure is
|
|
239
|
+
* not a fact about a live one.
|
|
240
|
+
*
|
|
241
|
+
* The probe is a token mint followed by `companyinformation`, and it must be both:
|
|
242
|
+
* minting alone proves the client pair and the consent, but only the company read proves
|
|
243
|
+
* the `TenantId` names a company this integration can actually see — which is the field
|
|
244
|
+
* an operator is most likely to paste wrong.
|
|
245
|
+
*/
|
|
246
|
+
export declare function probeFortnoxSecret(secret: Record<string, string>, options: FortnoxConnectorOptions): Promise<ConnectionProbe>;
|
|
247
|
+
/** Probe the credential a live connection already holds. Verifying is itself a use. */
|
|
248
|
+
export declare function probeFortnoxConnection(host: ScopeHost, connection: {
|
|
249
|
+
tenantId: string;
|
|
250
|
+
vertical: string;
|
|
251
|
+
}, options: FortnoxConnectorOptions): Promise<ConnectionProbe>;
|
|
252
|
+
/**
|
|
253
|
+
* What this connection has been doing, for a console — one entry per bound scope.
|
|
254
|
+
*
|
|
255
|
+
* Reads the binding ledger rather than the provider: this answers "what has the platform
|
|
256
|
+
* synced", which is the question an operator asks when a figure looks stale, and it
|
|
257
|
+
* answers it without spending a provider round trip.
|
|
258
|
+
*/
|
|
259
|
+
export declare function fortnoxConnectionActivity(host: ScopeHost, connectionId: ConnectionId): Promise<ConnectionActivity>;
|
|
260
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAOL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAEV,SAAS,EAET,SAAS,EACV,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EACL,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,aAAa,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,SAAS,EACT,YAAY,EACZ,SAAS,EACT,OAAO,EACP,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,yBAAyB,aAAc,CAAC;AAMrD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;OAOG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,QAAQ,CAAC,EAAE;QACT,eAAe,EAAE,MAAM,CAAC;QACxB,QAAQ,EAAE,MAAM,CAAC;QACjB;;;;;WAKG;QACH,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiC5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAKlE,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE;IACL,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,cAAc,CAAC,CA8BzB;AAED,8CAA8C;AAC9C,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,SAAS,EACf,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,cAAc,EAAE,CAAC,CAS3B;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,SAAS,EACf,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,iCAAiC;AACjC,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,uBAAuB,GAAG;IAAE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAC3E,OAAO,CAAC,iBAAiB,CAAC,CA2H5B;AAED,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,SAAS,EACf,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,uBAAuB,GAAG;IAAE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAC3E,OAAO,CAAC,kBAAkB,CAAC,CAsB7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,eAAe,CAAC,CA4B1B;AAED,uFAAuF;AACvF,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,SAAS,EACf,UAAU,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAClD,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAS1B;AAmDD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAmC7B"}
|