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.
Files changed (92) hide show
  1. package/README.md +37 -10
  2. package/dist/accounts/googleAuth.d.ts +31 -0
  3. package/dist/accounts/googleAuth.d.ts.map +1 -0
  4. package/dist/accounts/googleAuth.js +147 -0
  5. package/dist/accounts/googleAuth.js.map +1 -0
  6. package/dist/accounts/registry.d.ts +37 -0
  7. package/dist/accounts/registry.d.ts.map +1 -0
  8. package/dist/accounts/registry.js +278 -0
  9. package/dist/accounts/registry.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +629 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/config.d.ts +40 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +216 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/context.d.ts +15 -0
  19. package/dist/context.d.ts.map +1 -0
  20. package/dist/context.js +15 -0
  21. package/dist/context.js.map +1 -0
  22. package/dist/http/app.d.ts +16 -0
  23. package/dist/http/app.d.ts.map +1 -0
  24. package/dist/http/app.js +118 -0
  25. package/dist/http/app.js.map +1 -0
  26. package/dist/index.d.ts +14 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +14 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/licensing/activation.d.ts +5 -0
  31. package/dist/licensing/activation.d.ts.map +1 -0
  32. package/dist/licensing/activation.js +11 -0
  33. package/dist/licensing/activation.js.map +1 -0
  34. package/dist/licensing/client.d.ts +52 -0
  35. package/dist/licensing/client.d.ts.map +1 -0
  36. package/dist/licensing/client.js +75 -0
  37. package/dist/licensing/client.js.map +1 -0
  38. package/dist/licensing/entitlements.d.ts +55 -0
  39. package/dist/licensing/entitlements.d.ts.map +1 -0
  40. package/dist/licensing/entitlements.js +116 -0
  41. package/dist/licensing/entitlements.js.map +1 -0
  42. package/dist/licensing/lease.d.ts +47 -0
  43. package/dist/licensing/lease.d.ts.map +1 -0
  44. package/dist/licensing/lease.js +103 -0
  45. package/dist/licensing/lease.js.map +1 -0
  46. package/dist/licensing/refresher.d.ts +46 -0
  47. package/dist/licensing/refresher.d.ts.map +1 -0
  48. package/dist/licensing/refresher.js +150 -0
  49. package/dist/licensing/refresher.js.map +1 -0
  50. package/dist/mcp/buildServer.d.ts +28 -0
  51. package/dist/mcp/buildServer.d.ts.map +1 -0
  52. package/dist/mcp/buildServer.js +414 -0
  53. package/dist/mcp/buildServer.js.map +1 -0
  54. package/dist/scheduler/sendScheduler.d.ts +38 -0
  55. package/dist/scheduler/sendScheduler.d.ts.map +1 -0
  56. package/dist/scheduler/sendScheduler.js +128 -0
  57. package/dist/scheduler/sendScheduler.js.map +1 -0
  58. package/dist/service/emailService.d.ts +139 -0
  59. package/dist/service/emailService.d.ts.map +1 -0
  60. package/dist/service/emailService.js +380 -0
  61. package/dist/service/emailService.js.map +1 -0
  62. package/dist/storage/apiKeys.d.ts +27 -0
  63. package/dist/storage/apiKeys.d.ts.map +1 -0
  64. package/dist/storage/apiKeys.js +49 -0
  65. package/dist/storage/apiKeys.js.map +1 -0
  66. package/dist/storage/crypto.d.ts +4 -0
  67. package/dist/storage/crypto.d.ts.map +1 -0
  68. package/dist/storage/crypto.js +21 -0
  69. package/dist/storage/crypto.js.map +1 -0
  70. package/dist/storage/db.d.ts +1041 -0
  71. package/dist/storage/db.d.ts.map +1 -0
  72. package/dist/storage/db.js +211 -0
  73. package/dist/storage/db.js.map +1 -0
  74. package/dist/storage/imapState.d.ts +15 -0
  75. package/dist/storage/imapState.d.ts.map +1 -0
  76. package/dist/storage/imapState.js +89 -0
  77. package/dist/storage/imapState.js.map +1 -0
  78. package/dist/storage/members.d.ts +29 -0
  79. package/dist/storage/members.d.ts.map +1 -0
  80. package/dist/storage/members.js +67 -0
  81. package/dist/storage/members.js.map +1 -0
  82. package/dist/storage/scheduledSends.d.ts +60 -0
  83. package/dist/storage/scheduledSends.d.ts.map +1 -0
  84. package/dist/storage/scheduledSends.js +169 -0
  85. package/dist/storage/scheduledSends.js.map +1 -0
  86. package/dist/version.d.ts +2 -0
  87. package/dist/version.d.ts.map +1 -0
  88. package/dist/version.js +7 -0
  89. package/dist/version.js.map +1 -0
  90. package/package.json +49 -15
  91. package/bin/fluxmail.js +0 -4
  92. 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":"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"}
@@ -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"}