fluxmail 0.0.1 → 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 +37 -10
- package/dist/accounts/googleAuth.d.ts +31 -0
- package/dist/accounts/googleAuth.d.ts.map +1 -0
- package/dist/accounts/googleAuth.js +147 -0
- package/dist/accounts/googleAuth.js.map +1 -0
- package/dist/accounts/registry.d.ts +37 -0
- package/dist/accounts/registry.d.ts.map +1 -0
- package/dist/accounts/registry.js +278 -0
- package/dist/accounts/registry.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +629 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +216 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +15 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +15 -0
- package/dist/context.js.map +1 -0
- package/dist/http/app.d.ts +16 -0
- package/dist/http/app.d.ts.map +1 -0
- package/dist/http/app.js +118 -0
- package/dist/http/app.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/licensing/activation.d.ts +5 -0
- package/dist/licensing/activation.d.ts.map +1 -0
- package/dist/licensing/activation.js +11 -0
- package/dist/licensing/activation.js.map +1 -0
- package/dist/licensing/client.d.ts +52 -0
- package/dist/licensing/client.d.ts.map +1 -0
- package/dist/licensing/client.js +75 -0
- package/dist/licensing/client.js.map +1 -0
- package/dist/licensing/entitlements.d.ts +55 -0
- package/dist/licensing/entitlements.d.ts.map +1 -0
- package/dist/licensing/entitlements.js +116 -0
- package/dist/licensing/entitlements.js.map +1 -0
- package/dist/licensing/lease.d.ts +47 -0
- package/dist/licensing/lease.d.ts.map +1 -0
- package/dist/licensing/lease.js +103 -0
- package/dist/licensing/lease.js.map +1 -0
- package/dist/licensing/refresher.d.ts +46 -0
- package/dist/licensing/refresher.d.ts.map +1 -0
- package/dist/licensing/refresher.js +150 -0
- package/dist/licensing/refresher.js.map +1 -0
- package/dist/mcp/buildServer.d.ts +28 -0
- package/dist/mcp/buildServer.d.ts.map +1 -0
- package/dist/mcp/buildServer.js +414 -0
- package/dist/mcp/buildServer.js.map +1 -0
- package/dist/scheduler/sendScheduler.d.ts +38 -0
- package/dist/scheduler/sendScheduler.d.ts.map +1 -0
- package/dist/scheduler/sendScheduler.js +128 -0
- package/dist/scheduler/sendScheduler.js.map +1 -0
- package/dist/service/emailService.d.ts +139 -0
- package/dist/service/emailService.d.ts.map +1 -0
- package/dist/service/emailService.js +380 -0
- package/dist/service/emailService.js.map +1 -0
- package/dist/storage/apiKeys.d.ts +27 -0
- package/dist/storage/apiKeys.d.ts.map +1 -0
- package/dist/storage/apiKeys.js +49 -0
- package/dist/storage/apiKeys.js.map +1 -0
- package/dist/storage/crypto.d.ts +4 -0
- package/dist/storage/crypto.d.ts.map +1 -0
- package/dist/storage/crypto.js +21 -0
- package/dist/storage/crypto.js.map +1 -0
- package/dist/storage/db.d.ts +1041 -0
- package/dist/storage/db.d.ts.map +1 -0
- package/dist/storage/db.js +211 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/storage/imapState.d.ts +15 -0
- package/dist/storage/imapState.d.ts.map +1 -0
- package/dist/storage/imapState.js +89 -0
- package/dist/storage/imapState.js.map +1 -0
- package/dist/storage/members.d.ts +29 -0
- package/dist/storage/members.d.ts.map +1 -0
- package/dist/storage/members.js +67 -0
- package/dist/storage/members.js.map +1 -0
- package/dist/storage/scheduledSends.d.ts +60 -0
- package/dist/storage/scheduledSends.d.ts.map +1 -0
- package/dist/storage/scheduledSends.js +169 -0
- package/dist/storage/scheduledSends.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -15
- package/bin/fluxmail.js +0 -4
- package/src/cli.js +0 -36
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the hosted license server's validate and deactivate
|
|
3
|
+
* endpoints. The contract of record is docs/license-validation-contract.md in
|
|
4
|
+
* this repo, implemented by the private license-server repo; request/response
|
|
5
|
+
* shapes here must match it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_LICENSE_SERVER_URL = "https://fluxmail.ai";
|
|
8
|
+
export declare const LICENSE_KEY_PATTERN: RegExp;
|
|
9
|
+
export type ValidateOutcome =
|
|
10
|
+
/** 200: `lease` is the signed token; verify it before trusting anything in it. */
|
|
11
|
+
{
|
|
12
|
+
kind: 'valid';
|
|
13
|
+
lease: string;
|
|
14
|
+
}
|
|
15
|
+
/** 400: the server considered the request malformed — a config error to surface. */
|
|
16
|
+
| {
|
|
17
|
+
kind: 'invalid_request';
|
|
18
|
+
}
|
|
19
|
+
/** 404: key doesn't exist (typo or deleted). Keep any cached lease until it expires. */
|
|
20
|
+
| {
|
|
21
|
+
kind: 'license_not_found';
|
|
22
|
+
}
|
|
23
|
+
/** 403: subscription ended ("canceled" | "revoked" | "expired"). Degrade when the cached lease runs out. */
|
|
24
|
+
| {
|
|
25
|
+
kind: 'license_inactive';
|
|
26
|
+
status: string;
|
|
27
|
+
}
|
|
28
|
+
/** 409: the license is bound to a different instance; deactivate it there first. */
|
|
29
|
+
| {
|
|
30
|
+
kind: 'license_in_use';
|
|
31
|
+
}
|
|
32
|
+
/** 5xx, network failure, or an unintelligible response: keep the cached lease and retry later. */
|
|
33
|
+
| {
|
|
34
|
+
kind: 'outage';
|
|
35
|
+
detail: string;
|
|
36
|
+
};
|
|
37
|
+
export interface ValidateOptions {
|
|
38
|
+
serverUrl: string;
|
|
39
|
+
licenseKey: string;
|
|
40
|
+
/** Stable random id per install; the server binds each license to one instance. */
|
|
41
|
+
instanceId: string;
|
|
42
|
+
fetchImpl?: typeof fetch;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function validateLicense(opts: ValidateOptions): Promise<ValidateOutcome>;
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort release of this instance's license binding, so the key can be
|
|
48
|
+
* activated elsewhere. Returns false when the server could not be reached or
|
|
49
|
+
* declined; callers proceed with local deactivation either way.
|
|
50
|
+
*/
|
|
51
|
+
export declare function releaseLicense(opts: ValidateOptions): Promise<boolean>;
|
|
52
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/licensing/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,0BAA0B,wBAAwB,CAAC;AAEhE,eAAO,MAAM,mBAAmB,QAAgC,CAAC;AAEjE,MAAM,MAAM,eAAe;AACzB,kFAAkF;AAChF;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAClC,oFAAoF;GAClF;IAAE,IAAI,EAAE,iBAAiB,CAAA;CAAE;AAC7B,wFAAwF;GACtF;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE;AAC/B,4GAA4G;GAC1G;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC9C,oFAAoF;GAClF;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE;AAC5B,kGAAkG;GAChG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CA6CrF;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAc5E"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the hosted license server's validate and deactivate
|
|
3
|
+
* endpoints. The contract of record is docs/license-validation-contract.md in
|
|
4
|
+
* this repo, implemented by the private license-server repo; request/response
|
|
5
|
+
* shapes here must match it.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_LICENSE_SERVER_URL = 'https://fluxmail.ai';
|
|
8
|
+
export const LICENSE_KEY_PATTERN = /^fluxmail_lic_[0-9a-f]{40}$/;
|
|
9
|
+
export async function validateLicense(opts) {
|
|
10
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
11
|
+
const url = `${opts.serverUrl.replace(/\/+$/, '')}/api/v1/licenses/validate`;
|
|
12
|
+
let response;
|
|
13
|
+
try {
|
|
14
|
+
response = await doFetch(url, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { 'content-type': 'application/json' },
|
|
17
|
+
body: JSON.stringify({ licenseKey: opts.licenseKey, instanceId: opts.instanceId }),
|
|
18
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 10_000),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
return { kind: 'outage', detail: err instanceof Error ? err.message : String(err) };
|
|
23
|
+
}
|
|
24
|
+
let body;
|
|
25
|
+
try {
|
|
26
|
+
body = await response.json();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
body = undefined;
|
|
30
|
+
}
|
|
31
|
+
const field = (name) => typeof body === 'object' && body !== null ? body[name] : undefined;
|
|
32
|
+
switch (response.status) {
|
|
33
|
+
case 200: {
|
|
34
|
+
const lease = field('lease');
|
|
35
|
+
if (typeof lease !== 'string' || !lease) {
|
|
36
|
+
return { kind: 'outage', detail: 'validation response is missing the lease token' };
|
|
37
|
+
}
|
|
38
|
+
return { kind: 'valid', lease };
|
|
39
|
+
}
|
|
40
|
+
case 400:
|
|
41
|
+
return { kind: 'invalid_request' };
|
|
42
|
+
case 404:
|
|
43
|
+
return { kind: 'license_not_found' };
|
|
44
|
+
case 403: {
|
|
45
|
+
const status = field('status');
|
|
46
|
+
return { kind: 'license_inactive', status: typeof status === 'string' ? status : 'inactive' };
|
|
47
|
+
}
|
|
48
|
+
case 409:
|
|
49
|
+
return { kind: 'license_in_use' };
|
|
50
|
+
default:
|
|
51
|
+
return { kind: 'outage', detail: `license server returned HTTP ${response.status}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Best-effort release of this instance's license binding, so the key can be
|
|
56
|
+
* activated elsewhere. Returns false when the server could not be reached or
|
|
57
|
+
* declined; callers proceed with local deactivation either way.
|
|
58
|
+
*/
|
|
59
|
+
export async function releaseLicense(opts) {
|
|
60
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
61
|
+
const url = `${opts.serverUrl.replace(/\/+$/, '')}/api/v1/licenses/deactivate`;
|
|
62
|
+
try {
|
|
63
|
+
const response = await doFetch(url, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'content-type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ licenseKey: opts.licenseKey, instanceId: opts.instanceId }),
|
|
67
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 10_000),
|
|
68
|
+
});
|
|
69
|
+
return response.ok;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/licensing/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type FluxmailDb } from '../storage/db.js';
|
|
2
|
+
/** After a lease expires, paid limits are honored this much longer before the plan lapses. */
|
|
3
|
+
export declare const GRACE_PERIOD_MS: number;
|
|
4
|
+
export interface Entitlements {
|
|
5
|
+
/** 'personal' when unlicensed, otherwise the plan name from the signed lease. */
|
|
6
|
+
plan: string;
|
|
7
|
+
/** True while a lease grants paid limits (including the grace period). */
|
|
8
|
+
licensed: boolean;
|
|
9
|
+
/** True when the lease has expired but the grace period is still running. */
|
|
10
|
+
inGrace: boolean;
|
|
11
|
+
maxMembers: number;
|
|
12
|
+
maxAccounts: number;
|
|
13
|
+
/** Licensed only: when the lease expires (or expired) and the grace period starts. */
|
|
14
|
+
leaseExpiresAt?: string;
|
|
15
|
+
/** In grace only: when paid limits lapse to the Personal plan unless renewed. */
|
|
16
|
+
graceUntil?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const PERSONAL_TIER: Entitlements;
|
|
19
|
+
/** Accepts a transaction as well as the root db, like the other storage helpers. */
|
|
20
|
+
type DbReader = Pick<FluxmailDb, 'select'>;
|
|
21
|
+
export declare function readLeaseRow(db: DbReader): {
|
|
22
|
+
token: string;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
} | undefined;
|
|
25
|
+
export declare function saveLeaseToken(db: Pick<FluxmailDb, 'insert'>, token: string): void;
|
|
26
|
+
export declare function clearLease(db: Pick<FluxmailDb, 'delete'>): void;
|
|
27
|
+
/**
|
|
28
|
+
* Effective entitlements: the plan and caps from the cached lease while it
|
|
29
|
+
* verifies against a pinned license-server key, Personal-plan limits otherwise.
|
|
30
|
+
* An expired lease keeps its paid limits through the grace period, so a
|
|
31
|
+
* renewal hiccup never interrupts a running server. Reads only local state;
|
|
32
|
+
* enforcement never waits on the network.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getEntitlements(db: DbReader, now?: Date): Entitlements;
|
|
35
|
+
export declare function assertAccountLimit(current: number, entitlements: Entitlements): void;
|
|
36
|
+
export declare function assertMemberLimit(current: number, entitlements: Entitlements): void;
|
|
37
|
+
export interface LicenseState {
|
|
38
|
+
entitlements: Entitlements;
|
|
39
|
+
accountCount: number;
|
|
40
|
+
memberCount: number;
|
|
41
|
+
/** Usage exceeds the entitled caps; only possible after a paid license lapsed. */
|
|
42
|
+
overQuota: boolean;
|
|
43
|
+
/** Renewal warning while in grace or after a lapse; undefined when all is well. */
|
|
44
|
+
warning?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Entitlements plus current usage, for quota enforcement and renewal warnings. */
|
|
47
|
+
export declare function checkLicenseState(db: DbReader, now?: Date): LicenseState;
|
|
48
|
+
/**
|
|
49
|
+
* Gate for MCP tool calls: throws once a lapsed license leaves the instance
|
|
50
|
+
* over the entitled caps. Returns the current state so callers can surface
|
|
51
|
+
* grace warnings without a second read.
|
|
52
|
+
*/
|
|
53
|
+
export declare function assertWithinQuota(db: DbReader, now?: Date): LicenseState;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=entitlements.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlements.d.ts","sourceRoot":"","sources":["../../src/licensing/entitlements.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGpF,8FAA8F;AAC9F,eAAO,MAAM,eAAe,QAA2B,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,aAAa,EAAE,YAM3B,CAAC;AAIF,oFAAoF;AACpF,KAAK,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;AAE3C,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAG3F;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAMlF;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,GAAG,IAAI,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,OAAa,GAAG,YAAY,CAsB5E;AAcD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,CAEpF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,CAEnF;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,kFAAkF;IAClF,SAAS,EAAE,OAAO,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,OAAa,GAAG,YAAY,CAqB9E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,OAAa,GAAG,YAAY,CAa9E"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { EmailError } from '@fluxmail/core';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { accounts, licenseLease, members } from '../storage/db.js';
|
|
4
|
+
import { licensePublicKeys, verifyLease } from './lease.js';
|
|
5
|
+
/** After a lease expires, paid limits are honored this much longer before the plan lapses. */
|
|
6
|
+
export const GRACE_PERIOD_MS = 21 * 24 * 60 * 60 * 1000;
|
|
7
|
+
export const PERSONAL_TIER = {
|
|
8
|
+
plan: 'personal',
|
|
9
|
+
licensed: false,
|
|
10
|
+
inGrace: false,
|
|
11
|
+
maxMembers: 1,
|
|
12
|
+
maxAccounts: 3,
|
|
13
|
+
};
|
|
14
|
+
const LEASE_ROW_ID = 'current';
|
|
15
|
+
export function readLeaseRow(db) {
|
|
16
|
+
const row = db.select().from(licenseLease).where(eq(licenseLease.id, LEASE_ROW_ID)).get();
|
|
17
|
+
return row ? { token: row.token, updatedAt: row.updatedAt } : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function saveLeaseToken(db, token) {
|
|
20
|
+
const updatedAt = Date.now();
|
|
21
|
+
db.insert(licenseLease)
|
|
22
|
+
.values({ id: LEASE_ROW_ID, token, updatedAt })
|
|
23
|
+
.onConflictDoUpdate({ target: licenseLease.id, set: { token, updatedAt } })
|
|
24
|
+
.run();
|
|
25
|
+
}
|
|
26
|
+
export function clearLease(db) {
|
|
27
|
+
db.delete(licenseLease).where(eq(licenseLease.id, LEASE_ROW_ID)).run();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Effective entitlements: the plan and caps from the cached lease while it
|
|
31
|
+
* verifies against a pinned license-server key, Personal-plan limits otherwise.
|
|
32
|
+
* An expired lease keeps its paid limits through the grace period, so a
|
|
33
|
+
* renewal hiccup never interrupts a running server. Reads only local state;
|
|
34
|
+
* enforcement never waits on the network.
|
|
35
|
+
*/
|
|
36
|
+
export function getEntitlements(db, now = new Date()) {
|
|
37
|
+
const row = readLeaseRow(db);
|
|
38
|
+
if (!row)
|
|
39
|
+
return PERSONAL_TIER;
|
|
40
|
+
try {
|
|
41
|
+
const lease = verifyLease(row.token, licensePublicKeys(), now, { allowExpired: true });
|
|
42
|
+
const expiresAt = Date.parse(lease.expiresAt);
|
|
43
|
+
const graceUntil = expiresAt + GRACE_PERIOD_MS;
|
|
44
|
+
if (now.getTime() >= graceUntil)
|
|
45
|
+
return PERSONAL_TIER;
|
|
46
|
+
const inGrace = now.getTime() >= expiresAt;
|
|
47
|
+
return {
|
|
48
|
+
plan: lease.plan,
|
|
49
|
+
licensed: true,
|
|
50
|
+
inGrace,
|
|
51
|
+
maxMembers: lease.maxMembers,
|
|
52
|
+
maxAccounts: lease.maxAccounts,
|
|
53
|
+
leaseExpiresAt: lease.expiresAt,
|
|
54
|
+
...(inGrace ? { graceUntil: new Date(graceUntil).toISOString() } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Unverifiable lease: degrade to Personal-plan limits, never block.
|
|
59
|
+
return PERSONAL_TIER;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function assertLimit(kind, current, max, ent) {
|
|
63
|
+
if (current < max)
|
|
64
|
+
return;
|
|
65
|
+
const noun = `${kind === 'mailbox' ? 'connected mailbox' : 'member'}${max === 1 ? '' : kind === 'mailbox' ? 'es' : 's'}`;
|
|
66
|
+
throw new EmailError('entitlement_exceeded', ent.licensed
|
|
67
|
+
? `Your ${ent.plan} plan allows ${max} ${noun} (currently ${current}). Upgrade your plan to add more.`
|
|
68
|
+
: `The Personal plan allows ${max} ${noun} (currently ${current}). ` +
|
|
69
|
+
'A Fluxmail subscription unlocks more; see the README for details.');
|
|
70
|
+
}
|
|
71
|
+
export function assertAccountLimit(current, entitlements) {
|
|
72
|
+
assertLimit('mailbox', current, entitlements.maxAccounts, entitlements);
|
|
73
|
+
}
|
|
74
|
+
export function assertMemberLimit(current, entitlements) {
|
|
75
|
+
assertLimit('member', current, entitlements.maxMembers, entitlements);
|
|
76
|
+
}
|
|
77
|
+
/** Entitlements plus current usage, for quota enforcement and renewal warnings. */
|
|
78
|
+
export function checkLicenseState(db, now = new Date()) {
|
|
79
|
+
const entitlements = getEntitlements(db, now);
|
|
80
|
+
const accountCount = db.select().from(accounts).all().length;
|
|
81
|
+
const memberCount = db.select().from(members).all().length;
|
|
82
|
+
const overQuota = accountCount > entitlements.maxAccounts || memberCount > entitlements.maxMembers;
|
|
83
|
+
let warning;
|
|
84
|
+
if (entitlements.inGrace) {
|
|
85
|
+
warning =
|
|
86
|
+
`The Fluxmail license expired ${entitlements.leaseExpiresAt}; paid limits continue until ` +
|
|
87
|
+
`${entitlements.graceUntil}. Renew before then or this instance drops to the Personal plan.`;
|
|
88
|
+
}
|
|
89
|
+
else if (!entitlements.licensed && readLeaseRow(db)) {
|
|
90
|
+
warning =
|
|
91
|
+
`The Fluxmail license has lapsed; this instance is on the Personal plan ` +
|
|
92
|
+
`(${entitlements.maxAccounts} mailboxes, ${entitlements.maxMembers} member).` +
|
|
93
|
+
(overQuota
|
|
94
|
+
? ` Current usage (${accountCount} mailboxes, ${memberCount} members) exceeds that, so email tools are ` +
|
|
95
|
+
'blocked until you renew the license or remove mailboxes/members to fit.'
|
|
96
|
+
: '');
|
|
97
|
+
}
|
|
98
|
+
return { entitlements, accountCount, memberCount, overQuota, ...(warning ? { warning } : {}) };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Gate for MCP tool calls: throws once a lapsed license leaves the instance
|
|
102
|
+
* over the entitled caps. Returns the current state so callers can surface
|
|
103
|
+
* grace warnings without a second read.
|
|
104
|
+
*/
|
|
105
|
+
export function assertWithinQuota(db, now = new Date()) {
|
|
106
|
+
const state = checkLicenseState(db, now);
|
|
107
|
+
if (state.overQuota) {
|
|
108
|
+
const { entitlements: ent } = state;
|
|
109
|
+
throw new EmailError('entitlement_exceeded', `This instance has ${state.accountCount} connected mailboxes and ${state.memberCount} members, but the ` +
|
|
110
|
+
`${ent.plan} plan allows ${ent.maxAccounts} and ${ent.maxMembers}. Renew the license ` +
|
|
111
|
+
'("fluxmail license activate <key>") or remove mailboxes/members ' +
|
|
112
|
+
'("fluxmail accounts remove", "fluxmail members remove") to fit the plan.');
|
|
113
|
+
}
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=entitlements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlements.js","sourceRoot":"","sources":["../../src/licensing/entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAmB,MAAM,kBAAkB,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5D,8FAA8F;AAC9F,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAiBxD,MAAM,CAAC,MAAM,aAAa,GAAiB;IACzC,IAAI,EAAE,UAAU;IAChB,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,CAAC;CACf,CAAC;AAEF,MAAM,YAAY,GAAG,SAAS,CAAC;AAK/B,MAAM,UAAU,YAAY,CAAC,EAAY;IACvC,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAC1F,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAA8B,EAAE,KAAa;IAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC;SACpB,MAAM,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;SAC9C,kBAAkB,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC;SAC1E,GAAG,EAAE,CAAC;AACX,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAA8B;IACvD,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AACzE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,EAAY,EAAE,GAAG,GAAG,IAAI,IAAI,EAAE;IAC5D,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG;QAAE,OAAO,aAAa,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,GAAG,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QACvF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC9C,MAAM,UAAU,GAAG,SAAS,GAAG,eAAe,CAAC;QAC/C,IAAI,GAAG,CAAC,OAAO,EAAE,IAAI,UAAU;YAAE,OAAO,aAAa,CAAC;QACtD,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,EAAE,IAAI,SAAS,CAAC;QAC3C,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,IAAI;YACd,OAAO;YACP,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,cAAc,EAAE,KAAK,CAAC,SAAS;YAC/B,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,OAAO,aAAa,CAAC;IACvB,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,IAA0B,EAAE,OAAe,EAAE,GAAW,EAAE,GAAiB;IAC9F,IAAI,OAAO,GAAG,GAAG;QAAE,OAAO;IAC1B,MAAM,IAAI,GAAG,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,QAAQ,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IACzH,MAAM,IAAI,UAAU,CAClB,sBAAsB,EACtB,GAAG,CAAC,QAAQ;QACV,CAAC,CAAC,QAAQ,GAAG,CAAC,IAAI,gBAAgB,GAAG,IAAI,IAAI,eAAe,OAAO,mCAAmC;QACtG,CAAC,CAAC,4BAA4B,GAAG,IAAI,IAAI,eAAe,OAAO,KAAK;YAChE,mEAAmE,CAC1E,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,YAA0B;IAC5E,WAAW,CAAC,SAAS,EAAE,OAAO,EAAE,YAAY,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,YAA0B;IAC3E,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;AACxE,CAAC;AAYD,mFAAmF;AACnF,MAAM,UAAU,iBAAiB,CAAC,EAAY,EAAE,GAAG,GAAG,IAAI,IAAI,EAAE;IAC9D,MAAM,YAAY,GAAG,eAAe,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAC7D,MAAM,WAAW,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAC3D,MAAM,SAAS,GAAG,YAAY,GAAG,YAAY,CAAC,WAAW,IAAI,WAAW,GAAG,YAAY,CAAC,UAAU,CAAC;IAEnG,IAAI,OAA2B,CAAC;IAChC,IAAI,YAAY,CAAC,OAAO,EAAE,CAAC;QACzB,OAAO;YACL,gCAAgC,YAAY,CAAC,cAAc,+BAA+B;gBAC1F,GAAG,YAAY,CAAC,UAAU,kEAAkE,CAAC;IACjG,CAAC;SAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,IAAI,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC;QACtD,OAAO;YACL,yEAAyE;gBACzE,IAAI,YAAY,CAAC,WAAW,eAAe,YAAY,CAAC,UAAU,WAAW;gBAC7E,CAAC,SAAS;oBACR,CAAC,CAAC,mBAAmB,YAAY,eAAe,WAAW,6CAA6C;wBACtG,yEAAyE;oBAC3E,CAAC,CAAC,EAAE,CAAC,CAAC;IACZ,CAAC;IACD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACjG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAY,EAAE,GAAG,GAAG,IAAI,IAAI,EAAE;IAC9D,MAAM,KAAK,GAAG,iBAAiB,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IACzC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;QACpC,MAAM,IAAI,UAAU,CAClB,sBAAsB,EACtB,qBAAqB,KAAK,CAAC,YAAY,4BAA4B,KAAK,CAAC,WAAW,oBAAoB;YACtG,GAAG,GAAG,CAAC,IAAI,gBAAgB,GAAG,CAAC,WAAW,QAAQ,GAAG,CAAC,UAAU,sBAAsB;YACtF,kEAAkE;YAClE,0EAA0E,CAC7E,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entitlement lease issued by the hosted license server: a detached-signature
|
|
3
|
+
* token of the form base64url(payloadJSON) + "." + base64url(ed25519 signature).
|
|
4
|
+
* The signature covers the payload bytes exactly as transmitted, so we verify
|
|
5
|
+
* before parsing and never re-serialize the JSON.
|
|
6
|
+
*/
|
|
7
|
+
export interface LeasePayload {
|
|
8
|
+
/** Payload version; this client understands only 2. */
|
|
9
|
+
v: 2;
|
|
10
|
+
licenseId: string;
|
|
11
|
+
/** Lowercase plan name for display ("pro", "team", …); clients never switch on it. */
|
|
12
|
+
plan: string;
|
|
13
|
+
/** People who may use this instance. */
|
|
14
|
+
maxMembers: number;
|
|
15
|
+
/** Connected mailboxes on this instance. */
|
|
16
|
+
maxAccounts: number;
|
|
17
|
+
/** ISO 8601 */
|
|
18
|
+
issuedAt: string;
|
|
19
|
+
/** ISO 8601, issuedAt + ~7 days */
|
|
20
|
+
expiresAt: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* License-server signing keys (base64 SPKI DER), pinned at build time.
|
|
24
|
+
* Rotation: releases ship with both the old and new key pinned; the server
|
|
25
|
+
* switches its private key once instances have upgraded, then the old pin
|
|
26
|
+
* is removed.
|
|
27
|
+
*/
|
|
28
|
+
export declare const PINNED_LICENSE_PUBLIC_KEYS: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* Pinned keys plus any keys from FLUXMAIL_LICENSE_PUBLIC_KEYS (comma-separated
|
|
31
|
+
* base64 SPKI DER), which exists for development and tests. Extra keys are
|
|
32
|
+
* additive so an env typo can never unpin the production key, and the override
|
|
33
|
+
* is ignored under NODE_ENV=production (as set by the Docker image) so a stock
|
|
34
|
+
* production instance trusts only the pinned keys.
|
|
35
|
+
*/
|
|
36
|
+
export declare function licensePublicKeys(): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Verify a lease token against the accepted public keys and return its payload.
|
|
39
|
+
* Throws with a human-readable reason on any failure; callers on enforcement
|
|
40
|
+
* paths catch and fall back to Personal-plan limits. With allowExpired, an
|
|
41
|
+
* expired-but-authentic lease is returned so the caller can apply the grace
|
|
42
|
+
* period; signature and shape failures still throw.
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyLease(token: string, publicKeys: readonly string[], now?: Date, opts?: {
|
|
45
|
+
allowExpired?: boolean;
|
|
46
|
+
}): LeasePayload;
|
|
47
|
+
//# sourceMappingURL=lease.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../../src/licensing/lease.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,CAAC,EAAE,CAAC,CAAC;IACL,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EAGvD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAS5C;AAYD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,GAAG,OAAa,EAChB,IAAI,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACpC,YAAY,CAwDd"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createPublicKey, verify as verifySignature } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* License-server signing keys (base64 SPKI DER), pinned at build time.
|
|
4
|
+
* Rotation: releases ship with both the old and new key pinned; the server
|
|
5
|
+
* switches its private key once instances have upgraded, then the old pin
|
|
6
|
+
* is removed.
|
|
7
|
+
*/
|
|
8
|
+
export const PINNED_LICENSE_PUBLIC_KEYS = [
|
|
9
|
+
'MCowBQYDK2VwAyEAoxmkS0uM1qUX6+Jzm3aoTPLFuZ8mcpyiEI3VE1qdM90=',
|
|
10
|
+
'MCowBQYDK2VwAyEA3ZAEKMPlNE1Ne0bTra438WMvI2Tle+C2aDOOAA8ETWM=',
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Pinned keys plus any keys from FLUXMAIL_LICENSE_PUBLIC_KEYS (comma-separated
|
|
14
|
+
* base64 SPKI DER), which exists for development and tests. Extra keys are
|
|
15
|
+
* additive so an env typo can never unpin the production key, and the override
|
|
16
|
+
* is ignored under NODE_ENV=production (as set by the Docker image) so a stock
|
|
17
|
+
* production instance trusts only the pinned keys.
|
|
18
|
+
*/
|
|
19
|
+
export function licensePublicKeys() {
|
|
20
|
+
const extra = process.env.NODE_ENV === 'production' ? undefined : process.env.FLUXMAIL_LICENSE_PUBLIC_KEYS;
|
|
21
|
+
const fromEnv = extra
|
|
22
|
+
? extra
|
|
23
|
+
.split(',')
|
|
24
|
+
.map((key) => key.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
: [];
|
|
27
|
+
return [...PINNED_LICENSE_PUBLIC_KEYS, ...fromEnv];
|
|
28
|
+
}
|
|
29
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
30
|
+
function decodePublicKey(base64Spki) {
|
|
31
|
+
try {
|
|
32
|
+
return createPublicKey({ key: Buffer.from(base64Spki, 'base64'), format: 'der', type: 'spki' });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Verify a lease token against the accepted public keys and return its payload.
|
|
40
|
+
* Throws with a human-readable reason on any failure; callers on enforcement
|
|
41
|
+
* paths catch and fall back to Personal-plan limits. With allowExpired, an
|
|
42
|
+
* expired-but-authentic lease is returned so the caller can apply the grace
|
|
43
|
+
* period; signature and shape failures still throw.
|
|
44
|
+
*/
|
|
45
|
+
export function verifyLease(token, publicKeys, now = new Date(), opts = {}) {
|
|
46
|
+
const parts = token.split('.');
|
|
47
|
+
if (parts.length !== 2 || !BASE64URL.test(parts[0]) || !BASE64URL.test(parts[1])) {
|
|
48
|
+
throw new Error('malformed lease token');
|
|
49
|
+
}
|
|
50
|
+
const payloadBytes = Buffer.from(parts[0], 'base64url');
|
|
51
|
+
const signature = Buffer.from(parts[1], 'base64url');
|
|
52
|
+
const signed = publicKeys.some((key) => {
|
|
53
|
+
const publicKey = decodePublicKey(key);
|
|
54
|
+
if (!publicKey)
|
|
55
|
+
return false;
|
|
56
|
+
try {
|
|
57
|
+
return verifySignature(null, payloadBytes, publicKey, signature);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
if (!signed) {
|
|
64
|
+
throw new Error('lease signature does not match any accepted license key');
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(payloadBytes.toString('utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error('lease payload is not valid JSON');
|
|
72
|
+
}
|
|
73
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
74
|
+
throw new Error('lease payload is not an object');
|
|
75
|
+
}
|
|
76
|
+
const payload = parsed;
|
|
77
|
+
if (payload.v !== 2) {
|
|
78
|
+
throw new Error(`unsupported lease version ${JSON.stringify(payload.v)}`);
|
|
79
|
+
}
|
|
80
|
+
if (typeof payload.licenseId !== 'string' || !payload.licenseId) {
|
|
81
|
+
throw new Error('lease payload has no licenseId');
|
|
82
|
+
}
|
|
83
|
+
if (typeof payload.plan !== 'string' || !payload.plan) {
|
|
84
|
+
throw new Error('lease payload has no plan');
|
|
85
|
+
}
|
|
86
|
+
for (const field of ['maxMembers', 'maxAccounts']) {
|
|
87
|
+
const value = payload[field];
|
|
88
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
89
|
+
throw new Error(`lease payload has an invalid ${field}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const field of ['issuedAt', 'expiresAt']) {
|
|
93
|
+
const value = payload[field];
|
|
94
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) {
|
|
95
|
+
throw new Error(`lease payload has an invalid ${field}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!opts.allowExpired && Date.parse(payload.expiresAt) <= now.getTime()) {
|
|
99
|
+
throw new Error(`lease expired at ${payload.expiresAt}`);
|
|
100
|
+
}
|
|
101
|
+
return payload;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=lease.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease.js","sourceRoot":"","sources":["../../src/licensing/lease.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FluxmailConfig } from '../config.js';
|
|
2
|
+
import type { FluxmailDb } from '../storage/db.js';
|
|
3
|
+
import { type LeasePayload } from './lease.js';
|
|
4
|
+
/** Instances revalidate roughly daily; the lease itself is valid for ~7 days. */
|
|
5
|
+
export declare const VALIDATE_INTERVAL_MS: number;
|
|
6
|
+
export type RefreshResult = {
|
|
7
|
+
outcome: 'refreshed';
|
|
8
|
+
lease: LeasePayload;
|
|
9
|
+
} | {
|
|
10
|
+
outcome: 'invalid_key' | 'not_found' | 'inactive' | 'in_use' | 'bad_lease' | 'outage';
|
|
11
|
+
message: string;
|
|
12
|
+
/** True while a previously cached lease still grants paid limits. */
|
|
13
|
+
cachedLeaseActive: boolean;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Stable random id per install, persisted next to the database. Not a secret;
|
|
17
|
+
* it lets the license server spot one key shared across many instances.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadInstanceId(dataDir: string): string;
|
|
20
|
+
export interface RefreshOptions {
|
|
21
|
+
licenseKey: string;
|
|
22
|
+
serverUrl: string;
|
|
23
|
+
dataDir: string;
|
|
24
|
+
fetchImpl?: typeof fetch;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate the license with the server and cache the returned lease. Every
|
|
28
|
+
* failure keeps the existing cached lease, per the contract: an outage or a
|
|
29
|
+
* lapsed subscription degrades to free-tier limits only once the cached lease
|
|
30
|
+
* expires, and never interrupts a running server.
|
|
31
|
+
*/
|
|
32
|
+
export declare function refreshLicense(db: FluxmailDb, opts: RefreshOptions): Promise<RefreshResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Background revalidation for long-running server modes: refresh immediately
|
|
35
|
+
* if the last validation is over a day old, then keep a daily cadence. Timers
|
|
36
|
+
* are unref'd and every failure path is caught, so licensing can never keep
|
|
37
|
+
* the process alive or take it down. Returns a stop function.
|
|
38
|
+
*/
|
|
39
|
+
export declare function startLicenseRefresher(deps: {
|
|
40
|
+
db: FluxmailDb;
|
|
41
|
+
config: FluxmailConfig;
|
|
42
|
+
log?: (line: string) => void;
|
|
43
|
+
/** Notify long-lived services that renewed entitlements are available. */
|
|
44
|
+
onRefreshed?: () => void;
|
|
45
|
+
}): () => void;
|
|
46
|
+
//# sourceMappingURL=refresher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresher.d.ts","sourceRoot":"","sources":["../../src/licensing/refresher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAkC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/E,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,QAAsB,CAAC;AAIxD,MAAM,MAAM,aAAa,GACrB;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GAC7C;IACE,OAAO,EAAE,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,iBAAiB,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEN;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAStD;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA4DjG;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,EAAE,EAAE,UAAU,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,GAAG,MAAM,IAAI,CAoDb"}
|