@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/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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -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":"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"}