@kirjapro/mcp-server 0.5.11 → 0.6.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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/dist/accounts.d.ts +40 -0
- package/dist/accounts.js +45 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.js +245 -0
- package/dist/compliance/rates.d.ts +53 -0
- package/dist/compliance/rates.js +57 -0
- package/dist/database.types.d.ts +6525 -0
- package/dist/database.types.js +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +58 -52
- package/dist/lib/incomeStatementSynthesis.d.ts +47 -0
- package/dist/lib/incomeStatementSynthesis.js +140 -0
- package/dist/resources/company.d.ts +2 -0
- package/dist/resources/company.js +75 -0
- package/dist/resources/compliance.d.ts +2 -0
- package/dist/resources/compliance.js +338 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.js +10 -0
- package/dist/resources/schema.d.ts +2 -0
- package/dist/resources/schema.js +63 -0
- package/dist/resources/security.d.ts +2 -0
- package/dist/resources/security.js +33 -0
- package/dist/schemas.d.ts +12 -0
- package/dist/schemas.js +12 -0
- package/dist/tools/accounts.d.ts +2 -0
- package/dist/tools/accounts.js +48 -0
- package/dist/tools/banking.d.ts +2 -0
- package/dist/tools/banking.js +196 -0
- package/dist/tools/bookings.d.ts +2 -0
- package/dist/tools/bookings.js +294 -0
- package/dist/tools/customers.d.ts +2 -0
- package/dist/tools/customers.js +320 -0
- package/dist/tools/financialNotes.d.ts +2 -0
- package/dist/tools/financialNotes.js +255 -0
- package/dist/tools/fingerprints.d.ts +6 -0
- package/dist/tools/fingerprints.js +28 -0
- package/dist/tools/fiscal-years.d.ts +2 -0
- package/dist/tools/fiscal-years.js +35 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +105 -0
- package/dist/tools/invoices.d.ts +2 -0
- package/dist/tools/invoices.js +489 -0
- package/dist/tools/offers.d.ts +2 -0
- package/dist/tools/offers.js +443 -0
- package/dist/tools/received-invoices.d.ts +2 -0
- package/dist/tools/received-invoices.js +134 -0
- package/dist/tools/recurring-invoices.d.ts +2 -0
- package/dist/tools/recurring-invoices.js +348 -0
- package/dist/tools/reports.d.ts +2 -0
- package/dist/tools/reports.js +869 -0
- package/dist/tools/templates.d.ts +89 -0
- package/dist/tools/templates.js +636 -0
- package/dist/tools/transactions.d.ts +2 -0
- package/dist/tools/transactions.js +597 -0
- package/dist/tools/validation.d.ts +26 -0
- package/dist/tools/validation.js +127 -0
- package/dist/types.d.ts +388 -0
- package/dist/types.js +34 -0
- package/dist/utils.d.ts +276 -0
- package/dist/utils.js +637 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +3 -2
- package/server.json +5 -1
- package/tool-fingerprints.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-05-26
|
|
4
|
+
|
|
5
|
+
Syncs the published package with the 2026-05-22 offers/server-side migrations and fixes two latent bugs surfaced by an audit. Minor-bumped (not patch) because `kirjapro_update_offer_status` narrows its accepted input.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **`kirjapro_update_offer_status` (breaking input change)** — now delegates to the `record_offer_status_change` SECURITY DEFINER RPC (atomic status update + audit-event insert, accountant+ role gate, concurrency `not_claimable` guard). The accepted `status` enum is narrowed to `draft` / `accepted` / `rejected` / `expired`; `draft→sent` is no longer here — use the send-offer email path (the recipient gets the public link). **Why it matters:** migration `20260522200300_drop_offer_events_user_insert` dropped the user-side `offer_events` insert policy, so the previous direct `UPDATE + INSERT` path silently failed RLS on the audit row — flipping status with no audit trail. This release ships the fix that was committed but unreleased.
|
|
10
|
+
- **`tool-fingerprints.json`** — regenerated for the `update_offer_status` definition change (rug-pull guard baseline).
|
|
11
|
+
- **`reports.ts`** — income-statement template-line fetch now selects explicit columns instead of `select('*')` (clears the anti-pattern guard; the reference table is never returned to the agent, but the narrow select avoids exposing future schema additions).
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Onboarding prompt (`kirjapro-aloita`) showed every paying customer "Ilmainen".** It resolved the subscription tier with `.eq('company_id', …)`, but `subscriptions.company_id` is nullable and never populated by billing (all subscription writes are keyed by `user_id`). Now resolves via the `subscriptions_select` RLS policy (`user_id = auth.uid()`) with no company filter. Tier gating itself was always correct (enforced server-side in `mcp-auth` by `user_id`); only the cosmetic onboarding label was wrong.
|
|
16
|
+
- **API-key auth exchange had no network timeout.** A hung `mcp-auth` request could hang server startup forever, or permanently stall token refresh (the recursive refresh `tick()` awaited a promise that never rejected, so the backoff never re-armed and the JWT silently expired). The entire exchange — connect, headers, **and the response-body read** — is now bounded with `AbortSignal.timeout(10_000)` (the abort signal aborts the undici body stream too), and transport failures surface an actionable message instead of a raw `DOMException`. Covered by three new `auth.test.ts` cases (timeout, network failure, body-read timeout).
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`prepublishOnly` release gate** — `npm publish` now runs `check:types-drift → lint → test → check:antipatterns → build` first, so a stale fingerprint, a type error, a failing test, or a `select('*')` regression blocks the publish locally. (This repo runs no GitHub Actions CI by design — the gate lives at the real release moment instead.)
|
|
21
|
+
- **`server.json`** — documents the optional `KIRJAPRO_MCP_SURFACE` env var (`core` default = 26 tools, `full` = 52).
|
|
22
|
+
|
|
3
23
|
## [0.5.11] - 2026-05-18
|
|
4
24
|
|
|
5
25
|
Documentation-only release: removes named third-party brand references from the public-facing `README.md` and CHANGELOG entries, replacing them with category-level descriptions. No code or behavior changes — `dist/index.js` is byte-identical to v0.5.10 modulo the version string written by the prebuild hook.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ MCP server for Finnish accounting. Exposes Kirjapro bookkeeping, invoicing, and
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
9
|
- Node.js 20+
|
|
10
|
-
- Kirjapro account with **
|
|
10
|
+
- Kirjapro account on a paid tier with API access — **Yrittäjä**, **Kasvu**, **Tilitoimisto**, or **Räätälöity**
|
|
11
11
|
- API key (generate at Settings > API-avaimet)
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Finnish Chart of Accounts (YSE 2017) — commonly used account constants.
|
|
3
|
+
*
|
|
4
|
+
* MIRROR of src/lib/accounts.ts — kept in sync via mcp-server/src/accounts.test.ts.
|
|
5
|
+
* Do not modify without updating the source file.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ACCOUNTS: {
|
|
8
|
+
readonly MYYNTISAAMISET: "2210";
|
|
9
|
+
readonly ALV_SAAMISET: "2320";
|
|
10
|
+
readonly KASSA: "2910";
|
|
11
|
+
readonly PANKKITILI: "2920";
|
|
12
|
+
readonly OSTOVELAT: "4130";
|
|
13
|
+
readonly ALV_VELKA: "4210";
|
|
14
|
+
readonly ENNAKONPIDATYSVELKA: "4220";
|
|
15
|
+
readonly TYEL_VELKA: "4240";
|
|
16
|
+
readonly SOTU_VELKA: "4230";
|
|
17
|
+
readonly TTV_VELKA: "4250";
|
|
18
|
+
readonly PALKKAVELKA: "4420";
|
|
19
|
+
readonly MYYNTI_25_5: "5010";
|
|
20
|
+
readonly MYYNTI_13_5: "5020";
|
|
21
|
+
readonly MYYNTI_10: "5030";
|
|
22
|
+
readonly MYYNTI_0: "5040";
|
|
23
|
+
readonly EU_TAVARAT: "5050";
|
|
24
|
+
readonly EU_PALVELUT: "5060";
|
|
25
|
+
readonly VIENTI: "5070";
|
|
26
|
+
readonly OSTOT: "6010";
|
|
27
|
+
readonly OSTOT_PALVELUT: "6030";
|
|
28
|
+
readonly PALKAT: "7010";
|
|
29
|
+
readonly ELAKEKULUT: "7100";
|
|
30
|
+
readonly SOSIAALITURVAMAKSUT: "7210";
|
|
31
|
+
readonly TOIMITILAVUOKRAT: "7420";
|
|
32
|
+
readonly SAHKO: "7510";
|
|
33
|
+
readonly POLTTOAINEET: "7610";
|
|
34
|
+
readonly EDUSTUSKULUT: "7810";
|
|
35
|
+
readonly PUHELIN: "7951";
|
|
36
|
+
readonly TOIMISTOTARVIKKEET: "7952";
|
|
37
|
+
readonly OHJELMISTOT: "7954";
|
|
38
|
+
readonly PANKKIKULUT: "7957";
|
|
39
|
+
readonly VAKUUTUKSET: "7970";
|
|
40
|
+
};
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Finnish Chart of Accounts (YSE 2017) — commonly used account constants.
|
|
3
|
+
*
|
|
4
|
+
* MIRROR of src/lib/accounts.ts — kept in sync via mcp-server/src/accounts.test.ts.
|
|
5
|
+
* Do not modify without updating the source file.
|
|
6
|
+
*/
|
|
7
|
+
export const ACCOUNTS = {
|
|
8
|
+
// ── Assets (2xxx) ──
|
|
9
|
+
MYYNTISAAMISET: '2210',
|
|
10
|
+
ALV_SAAMISET: '2320',
|
|
11
|
+
KASSA: '2910',
|
|
12
|
+
PANKKITILI: '2920',
|
|
13
|
+
// ── Liabilities (4xxx) ──
|
|
14
|
+
OSTOVELAT: '4130',
|
|
15
|
+
ALV_VELKA: '4210',
|
|
16
|
+
ENNAKONPIDATYSVELKA: '4220',
|
|
17
|
+
TYEL_VELKA: '4240',
|
|
18
|
+
SOTU_VELKA: '4230',
|
|
19
|
+
TTV_VELKA: '4250',
|
|
20
|
+
PALKKAVELKA: '4420',
|
|
21
|
+
// ── Revenue (5xxx) ──
|
|
22
|
+
MYYNTI_25_5: '5010',
|
|
23
|
+
MYYNTI_13_5: '5020',
|
|
24
|
+
MYYNTI_10: '5030',
|
|
25
|
+
MYYNTI_0: '5040',
|
|
26
|
+
EU_TAVARAT: '5050',
|
|
27
|
+
EU_PALVELUT: '5060',
|
|
28
|
+
VIENTI: '5070',
|
|
29
|
+
// ── Materials & Services (6xxx) ──
|
|
30
|
+
OSTOT: '6010',
|
|
31
|
+
OSTOT_PALVELUT: '6030',
|
|
32
|
+
// ── Expenses (7xxx) ──
|
|
33
|
+
PALKAT: '7010',
|
|
34
|
+
ELAKEKULUT: '7100',
|
|
35
|
+
SOSIAALITURVAMAKSUT: '7210',
|
|
36
|
+
TOIMITILAVUOKRAT: '7420',
|
|
37
|
+
SAHKO: '7510',
|
|
38
|
+
POLTTOAINEET: '7610',
|
|
39
|
+
EDUSTUSKULUT: '7810',
|
|
40
|
+
PUHELIN: '7951',
|
|
41
|
+
TOIMISTOTARVIKKEET: '7952',
|
|
42
|
+
OHJELMISTOT: '7954',
|
|
43
|
+
PANKKIKULUT: '7957',
|
|
44
|
+
VAKUUTUKSET: '7970',
|
|
45
|
+
};
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { Database } from './database.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Cancel any pending token refresh and release the event loop.
|
|
5
|
+
* Call from `transport.onclose` so the MCP process exits cleanly.
|
|
6
|
+
*/
|
|
7
|
+
export declare function stopTokenRefresh(): void;
|
|
8
|
+
/**
|
|
9
|
+
* Initialize authenticated Supabase client.
|
|
10
|
+
*
|
|
11
|
+
* Supports two auth modes:
|
|
12
|
+
* 1. API key (recommended): Set KIRJAPRO_API_KEY
|
|
13
|
+
* 2. Legacy (development): Set SUPABASE_URL + SUPABASE_ACCESS_TOKEN + KIRJAPRO_COMPANY_ID
|
|
14
|
+
*/
|
|
15
|
+
export declare function initAuth(): Promise<void>;
|
|
16
|
+
export declare function getSupabase(): SupabaseClient<Database>;
|
|
17
|
+
export declare function getCompanyId(): string;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
const SUPABASE_PROJECT_URL = 'https://tifewzsimmyoiepblhld.supabase.co';
|
|
3
|
+
let supabase = null;
|
|
4
|
+
let companyId = null;
|
|
5
|
+
let refreshTimer = null;
|
|
6
|
+
/**
|
|
7
|
+
* Authenticate via API key by calling the mcp-auth edge function.
|
|
8
|
+
* Returns the auth response containing a short-lived JWT.
|
|
9
|
+
*/
|
|
10
|
+
async function authenticateWithApiKey(apiKey) {
|
|
11
|
+
const url = `${SUPABASE_PROJECT_URL}/functions/v1/mcp-auth`;
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({ api_key: apiKey }),
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
const body = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
19
|
+
throw new Error(`API key authentication failed (${response.status}): ${body.error || 'Unknown error'}`);
|
|
20
|
+
}
|
|
21
|
+
return response.json();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a Supabase client from an mcp-auth response.
|
|
25
|
+
*/
|
|
26
|
+
function createClientFromAuth(auth) {
|
|
27
|
+
return createClient(auth.supabase_url, auth.anon_key, {
|
|
28
|
+
auth: {
|
|
29
|
+
persistSession: false,
|
|
30
|
+
autoRefreshToken: false,
|
|
31
|
+
},
|
|
32
|
+
global: {
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer ${auth.access_token}`,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Schedule automatic token refresh before the JWT expires.
|
|
41
|
+
*
|
|
42
|
+
* Hardening (see audit M2):
|
|
43
|
+
* - Jitter 75–90% of TTL: multiple concurrent agents sharing one API key
|
|
44
|
+
* don't hit `mcp-auth` in lockstep (10 req/min rate limit).
|
|
45
|
+
* - Recursive `setTimeout`: re-arms with the *new* `expires_in` after every
|
|
46
|
+
* successful refresh (can't do that with `setInterval`).
|
|
47
|
+
* - Exponential backoff (capped at 60s, jittered) on refresh failure.
|
|
48
|
+
* - `.unref()`: doesn't keep the event loop alive if stdio transport closes.
|
|
49
|
+
*/
|
|
50
|
+
function scheduleTokenRefresh(apiKey, expiresIn) {
|
|
51
|
+
if (refreshTimer) {
|
|
52
|
+
clearTimeout(refreshTimer);
|
|
53
|
+
}
|
|
54
|
+
const ttlMs = expiresIn * 1000;
|
|
55
|
+
// Refresh at 75–90% of TTL, clamped to [60s, ttl-60s]
|
|
56
|
+
const target = ttlMs * (0.75 + Math.random() * 0.15);
|
|
57
|
+
const refreshMs = Math.max(60_000, Math.min(target, ttlMs - 60_000));
|
|
58
|
+
const tick = async (attempt) => {
|
|
59
|
+
try {
|
|
60
|
+
console.error('[kirjapro-mcp] Refreshing API key token...');
|
|
61
|
+
const auth = await authenticateWithApiKey(apiKey);
|
|
62
|
+
supabase = createClientFromAuth(auth);
|
|
63
|
+
companyId = auth.company_id;
|
|
64
|
+
console.error('[kirjapro-mcp] Token refreshed successfully');
|
|
65
|
+
scheduleTokenRefresh(apiKey, auth.expires_in);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('[kirjapro-mcp] Token refresh failed:', error.message);
|
|
69
|
+
// Exponential backoff with jitter: 1s, 2s, 4s, …, capped at 60s.
|
|
70
|
+
const backoff = Math.min(60_000, 2 ** attempt * 1000) + Math.random() * 1000;
|
|
71
|
+
refreshTimer = setTimeout(() => {
|
|
72
|
+
void tick(attempt + 1);
|
|
73
|
+
}, backoff);
|
|
74
|
+
refreshTimer.unref?.();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
refreshTimer = setTimeout(() => {
|
|
78
|
+
void tick(0);
|
|
79
|
+
}, refreshMs);
|
|
80
|
+
refreshTimer.unref?.();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Cancel any pending token refresh and release the event loop.
|
|
84
|
+
* Call from `transport.onclose` so the MCP process exits cleanly.
|
|
85
|
+
*/
|
|
86
|
+
export function stopTokenRefresh() {
|
|
87
|
+
if (refreshTimer) {
|
|
88
|
+
clearTimeout(refreshTimer);
|
|
89
|
+
refreshTimer = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Initialize using KIRJAPRO_API_KEY (recommended).
|
|
94
|
+
* Calls the mcp-auth edge function to exchange the API key for a JWT.
|
|
95
|
+
*/
|
|
96
|
+
async function initApiKeyAuth(apiKey) {
|
|
97
|
+
const auth = await authenticateWithApiKey(apiKey);
|
|
98
|
+
supabase = createClientFromAuth(auth);
|
|
99
|
+
companyId = auth.company_id;
|
|
100
|
+
// Validate the connection by fetching company info
|
|
101
|
+
const { data: company, error: companyError } = await supabase
|
|
102
|
+
.from('companies')
|
|
103
|
+
.select('id, name')
|
|
104
|
+
.eq('id', companyId)
|
|
105
|
+
.single();
|
|
106
|
+
if (companyError || !company) {
|
|
107
|
+
throw new Error(`Cannot access company ${companyId}. ${companyError?.message || ''}`);
|
|
108
|
+
}
|
|
109
|
+
console.error(`[kirjapro-mcp] Authenticated via API key | Company: ${company.name}`);
|
|
110
|
+
// Schedule automatic token refresh
|
|
111
|
+
scheduleTokenRefresh(apiKey, auth.expires_in);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Initialize using legacy env vars (for development).
|
|
115
|
+
* Requires SUPABASE_URL, SUPABASE_ACCESS_TOKEN, KIRJAPRO_COMPANY_ID.
|
|
116
|
+
*/
|
|
117
|
+
async function initLegacyAuth() {
|
|
118
|
+
const supabaseUrl = process.env.SUPABASE_URL;
|
|
119
|
+
const supabaseAccessToken = process.env.SUPABASE_ACCESS_TOKEN;
|
|
120
|
+
const kirjaproCompanyId = process.env.KIRJAPRO_COMPANY_ID;
|
|
121
|
+
if (!supabaseUrl) {
|
|
122
|
+
throw new Error('SUPABASE_URL environment variable is required.');
|
|
123
|
+
}
|
|
124
|
+
if (!supabaseAccessToken) {
|
|
125
|
+
throw new Error('SUPABASE_ACCESS_TOKEN environment variable is required.');
|
|
126
|
+
}
|
|
127
|
+
if (!kirjaproCompanyId) {
|
|
128
|
+
throw new Error('KIRJAPRO_COMPANY_ID environment variable is required.');
|
|
129
|
+
}
|
|
130
|
+
// Use anon key if available, otherwise fall back to access token (works but semantically wrong)
|
|
131
|
+
const anonKey = process.env.SUPABASE_ANON_KEY || supabaseAccessToken;
|
|
132
|
+
supabase = createClient(supabaseUrl, anonKey, {
|
|
133
|
+
auth: {
|
|
134
|
+
persistSession: false,
|
|
135
|
+
autoRefreshToken: false,
|
|
136
|
+
},
|
|
137
|
+
global: {
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: `Bearer ${supabaseAccessToken}`,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
// Validate the token
|
|
144
|
+
const { data: userData, error: authError } = await supabase.auth.getUser();
|
|
145
|
+
if (authError || !userData.user) {
|
|
146
|
+
throw new Error('Failed to authenticate with Supabase. ' +
|
|
147
|
+
'Token may be expired. Refresh your access token and restart the MCP server. ' +
|
|
148
|
+
(authError?.message || ''));
|
|
149
|
+
}
|
|
150
|
+
// Verify company access
|
|
151
|
+
const { data: company, error: companyError } = await supabase
|
|
152
|
+
.from('companies')
|
|
153
|
+
.select('id, name')
|
|
154
|
+
.eq('id', kirjaproCompanyId)
|
|
155
|
+
.single();
|
|
156
|
+
if (companyError || !company) {
|
|
157
|
+
throw new Error(`Cannot access company ${kirjaproCompanyId}. ` +
|
|
158
|
+
'Verify the KIRJAPRO_COMPANY_ID is correct and your user has access. ' +
|
|
159
|
+
(companyError?.message || ''));
|
|
160
|
+
}
|
|
161
|
+
// Check subscription tier for API access (subscriptions are user-keyed)
|
|
162
|
+
const { data: subscription } = await supabase
|
|
163
|
+
.from('subscriptions')
|
|
164
|
+
.select('tier')
|
|
165
|
+
.eq('user_id', userData.user.id)
|
|
166
|
+
.single();
|
|
167
|
+
const tier = subscription?.tier || 'free';
|
|
168
|
+
// Keep in sync with feature-gates.ts (hasApiAccess) + supabase/functions/mcp-auth.
|
|
169
|
+
// 'growth' (Kasvu) is a paid tier with API access — DT-073.
|
|
170
|
+
const apiTiers = ['entrepreneur', 'growth', 'company', 'custom'];
|
|
171
|
+
if (!apiTiers.includes(tier)) {
|
|
172
|
+
throw new Error(`API access requires a Yrittäjä, Kasvu, Tilitoimisto, or Räätälöity subscription. ` +
|
|
173
|
+
`Current tier: ${tier}. Upgrade at https://kirjapro.fi/settings/billing`);
|
|
174
|
+
}
|
|
175
|
+
companyId = kirjaproCompanyId;
|
|
176
|
+
console.error(`[kirjapro-mcp] Authenticated | Company: ${company.name} | Tier: ${tier}`);
|
|
177
|
+
// Parse JWT exp and schedule an expiry warning — legacy auth has no
|
|
178
|
+
// refresh path, so warn the user before the session silently dies.
|
|
179
|
+
try {
|
|
180
|
+
const payload = JSON.parse(Buffer.from(supabaseAccessToken.split('.')[1], 'base64url').toString());
|
|
181
|
+
if (typeof payload.exp === 'number') {
|
|
182
|
+
const expiresInMs = payload.exp * 1000 - Date.now();
|
|
183
|
+
if (expiresInMs > 0 && expiresInMs < 24 * 60 * 60 * 1000) {
|
|
184
|
+
const warnMs = Math.max(0, expiresInMs - 5 * 60 * 1000);
|
|
185
|
+
const timer = setTimeout(() => {
|
|
186
|
+
console.error('[kirjapro-mcp] WARNING: Legacy auth token expires in ~5 minutes. ' +
|
|
187
|
+
'Restart with a fresh token or switch to KIRJAPRO_API_KEY for automatic refresh.');
|
|
188
|
+
}, warnMs);
|
|
189
|
+
timer.unref?.();
|
|
190
|
+
console.error(`[kirjapro-mcp] Token expires in ${Math.round(expiresInMs / 60_000)} minutes. ` +
|
|
191
|
+
'Consider switching to KIRJAPRO_API_KEY for automatic refresh.');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Not a JWT or unparseable — service_role keys don't expire
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Initialize authenticated Supabase client.
|
|
201
|
+
*
|
|
202
|
+
* Supports two auth modes:
|
|
203
|
+
* 1. API key (recommended): Set KIRJAPRO_API_KEY
|
|
204
|
+
* 2. Legacy (development): Set SUPABASE_URL + SUPABASE_ACCESS_TOKEN + KIRJAPRO_COMPANY_ID
|
|
205
|
+
*/
|
|
206
|
+
export async function initAuth() {
|
|
207
|
+
const apiKey = process.env.KIRJAPRO_API_KEY;
|
|
208
|
+
if (apiKey) {
|
|
209
|
+
if (!apiKey.startsWith('kp_')) {
|
|
210
|
+
throw new Error('Invalid KIRJAPRO_API_KEY format. Keys start with "kp_". ' +
|
|
211
|
+
'Generate one at https://kirjapro.fi/settings/api-keys');
|
|
212
|
+
}
|
|
213
|
+
await initApiKeyAuth(apiKey);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Legacy env vars present?
|
|
217
|
+
if (process.env.SUPABASE_URL && process.env.SUPABASE_ACCESS_TOKEN && process.env.KIRJAPRO_COMPANY_ID) {
|
|
218
|
+
if (process.env.ALLOW_LEGACY_AUTH !== 'true') {
|
|
219
|
+
throw new Error('Legacy auth (SUPABASE_URL + SUPABASE_ACCESS_TOKEN + KIRJAPRO_COMPANY_ID) is disabled.\n\n' +
|
|
220
|
+
'Legacy auth bypasses the mcp-auth exchange — no audit log, no IP trail, no key rotation signal.\n' +
|
|
221
|
+
'Switch to KIRJAPRO_API_KEY: generate a key at https://kirjapro.fi/settings/api-keys.\n\n' +
|
|
222
|
+
'If you must use legacy auth temporarily (e.g. local development against a dev project),\n' +
|
|
223
|
+
'set ALLOW_LEGACY_AUTH=true to opt in. This path will be removed in a future release.');
|
|
224
|
+
}
|
|
225
|
+
console.error('[kirjapro-mcp] WARNING: Using legacy auth. This bypasses audit logging and will be removed ' +
|
|
226
|
+
'in a future release. Migrate to KIRJAPRO_API_KEY: https://kirjapro.fi/settings/api-keys');
|
|
227
|
+
await initLegacyAuth();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
throw new Error('Missing authentication configuration.\n\n' +
|
|
231
|
+
'Set KIRJAPRO_API_KEY (recommended):\n' +
|
|
232
|
+
' Generate an API key at https://kirjapro.fi/settings/api-keys');
|
|
233
|
+
}
|
|
234
|
+
export function getSupabase() {
|
|
235
|
+
if (!supabase) {
|
|
236
|
+
throw new Error('Supabase client not initialized. Call initAuth() first.');
|
|
237
|
+
}
|
|
238
|
+
return supabase;
|
|
239
|
+
}
|
|
240
|
+
export function getCompanyId() {
|
|
241
|
+
if (!companyId) {
|
|
242
|
+
throw new Error('Company ID not set. Call initAuth() first.');
|
|
243
|
+
}
|
|
244
|
+
return companyId;
|
|
245
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for Finnish tax + social insurance rates.
|
|
3
|
+
*
|
|
4
|
+
* Every tool that needs a rate and every compliance resource that publishes
|
|
5
|
+
* a rate MUST import from this module — do NOT re-declare rates elsewhere.
|
|
6
|
+
* When rates change for a new year, update this file and bump `sourcesVerifiedAt`.
|
|
7
|
+
*
|
|
8
|
+
* Sources:
|
|
9
|
+
* - VAT: AVL 1501/1993 §§ 84–85a, vero.fi/syventavat
|
|
10
|
+
* - TyEL: https://www.etk.fi/ajankohtaista/tyoelakemaksut-vuonna-2026/
|
|
11
|
+
* - Elo 2026: https://www.elo.fi/fi-fi/tyonantaja/tyel-vakuuttaminen/tyel-maksu/sosiaalivakuutusmaksut-2026
|
|
12
|
+
* - Sotu/TTV: tyel-vakuutus.fi/tyonantajan-sivukulut/
|
|
13
|
+
*/
|
|
14
|
+
export declare const FINNISH_RATES_2026: {
|
|
15
|
+
readonly year: 2026;
|
|
16
|
+
readonly validFrom: "2026-01-01";
|
|
17
|
+
readonly validUntil: "2026-12-31";
|
|
18
|
+
readonly sourcesVerifiedAt: "2026-03-15";
|
|
19
|
+
readonly source: "AVL 1501/1993, ETK 2026, Elo 2026, vero.fi 2026";
|
|
20
|
+
readonly vat: {
|
|
21
|
+
readonly standard: 25.5;
|
|
22
|
+
readonly reduced: 13.5;
|
|
23
|
+
readonly secondReduced: 10;
|
|
24
|
+
readonly zero: 0;
|
|
25
|
+
};
|
|
26
|
+
readonly social: {
|
|
27
|
+
readonly tyelEmployee: 7.3;
|
|
28
|
+
readonly tyelEmployer: 17.55;
|
|
29
|
+
readonly tyelEmployerAverage: 17.1;
|
|
30
|
+
readonly tyelKeskimaarainen: 24.4;
|
|
31
|
+
readonly tyelPerusmaksu: 24.85;
|
|
32
|
+
readonly sotu: 1.91;
|
|
33
|
+
readonly ttvEmployer: 0.31;
|
|
34
|
+
readonly ttvEmployerLarge: 1.23;
|
|
35
|
+
readonly ttvEmployerLargeThreshold: 2509500;
|
|
36
|
+
readonly ttvEmployee: 0.89;
|
|
37
|
+
readonly tapaturmaAverage: 0.51;
|
|
38
|
+
readonly ryhmahenki: 0.06;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export type FinnishRates = typeof FINNISH_RATES_2026;
|
|
42
|
+
/**
|
|
43
|
+
* Compute freshness metadata for a dated resource. `today` is injectable for
|
|
44
|
+
* tests; production callers pass nothing and get the current date.
|
|
45
|
+
*/
|
|
46
|
+
export declare function freshnessMeta(rates?: FinnishRates, today?: Date): {
|
|
47
|
+
published_at: string;
|
|
48
|
+
valid_from: string;
|
|
49
|
+
valid_through: string;
|
|
50
|
+
sources_verified_at: string;
|
|
51
|
+
stale: boolean;
|
|
52
|
+
days_until_expiry: number;
|
|
53
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for Finnish tax + social insurance rates.
|
|
3
|
+
*
|
|
4
|
+
* Every tool that needs a rate and every compliance resource that publishes
|
|
5
|
+
* a rate MUST import from this module — do NOT re-declare rates elsewhere.
|
|
6
|
+
* When rates change for a new year, update this file and bump `sourcesVerifiedAt`.
|
|
7
|
+
*
|
|
8
|
+
* Sources:
|
|
9
|
+
* - VAT: AVL 1501/1993 §§ 84–85a, vero.fi/syventavat
|
|
10
|
+
* - TyEL: https://www.etk.fi/ajankohtaista/tyoelakemaksut-vuonna-2026/
|
|
11
|
+
* - Elo 2026: https://www.elo.fi/fi-fi/tyonantaja/tyel-vakuuttaminen/tyel-maksu/sosiaalivakuutusmaksut-2026
|
|
12
|
+
* - Sotu/TTV: tyel-vakuutus.fi/tyonantajan-sivukulut/
|
|
13
|
+
*/
|
|
14
|
+
export const FINNISH_RATES_2026 = {
|
|
15
|
+
year: 2026,
|
|
16
|
+
validFrom: '2026-01-01',
|
|
17
|
+
validUntil: '2026-12-31',
|
|
18
|
+
sourcesVerifiedAt: '2026-03-15',
|
|
19
|
+
source: 'AVL 1501/1993, ETK 2026, Elo 2026, vero.fi 2026',
|
|
20
|
+
vat: {
|
|
21
|
+
standard: 25.5,
|
|
22
|
+
reduced: 13.5,
|
|
23
|
+
secondReduced: 10,
|
|
24
|
+
zero: 0,
|
|
25
|
+
},
|
|
26
|
+
social: {
|
|
27
|
+
tyelEmployee: 7.30,
|
|
28
|
+
tyelEmployer: 17.55,
|
|
29
|
+
tyelEmployerAverage: 17.10,
|
|
30
|
+
tyelKeskimaarainen: 24.40,
|
|
31
|
+
tyelPerusmaksu: 24.85,
|
|
32
|
+
sotu: 1.91,
|
|
33
|
+
ttvEmployer: 0.31,
|
|
34
|
+
ttvEmployerLarge: 1.23,
|
|
35
|
+
ttvEmployerLargeThreshold: 2_509_500,
|
|
36
|
+
ttvEmployee: 0.89,
|
|
37
|
+
tapaturmaAverage: 0.51,
|
|
38
|
+
ryhmahenki: 0.06,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Compute freshness metadata for a dated resource. `today` is injectable for
|
|
43
|
+
* tests; production callers pass nothing and get the current date.
|
|
44
|
+
*/
|
|
45
|
+
export function freshnessMeta(rates = FINNISH_RATES_2026, today = new Date()) {
|
|
46
|
+
const validUntil = new Date(`${rates.validUntil}T23:59:59Z`);
|
|
47
|
+
const msPerDay = 86_400_000;
|
|
48
|
+
const daysUntilExpiry = Math.ceil((validUntil.getTime() - today.getTime()) / msPerDay);
|
|
49
|
+
return {
|
|
50
|
+
published_at: rates.sourcesVerifiedAt,
|
|
51
|
+
valid_from: rates.validFrom,
|
|
52
|
+
valid_through: rates.validUntil,
|
|
53
|
+
sources_verified_at: rates.sourcesVerifiedAt,
|
|
54
|
+
stale: daysUntilExpiry < 0,
|
|
55
|
+
days_until_expiry: daysUntilExpiry,
|
|
56
|
+
};
|
|
57
|
+
}
|