@kernhq/module-billing 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 (46) hide show
  1. package/LICENSE +662 -0
  2. package/dist/contract.d.ts +965 -0
  3. package/dist/contract.d.ts.map +1 -0
  4. package/dist/contract.js +265 -0
  5. package/dist/contract.js.map +1 -0
  6. package/dist/server/index.d.ts +11 -0
  7. package/dist/server/index.d.ts.map +1 -0
  8. package/dist/server/index.js +165 -0
  9. package/dist/server/index.js.map +1 -0
  10. package/dist/server/router.d.ts +697 -0
  11. package/dist/server/router.d.ts.map +1 -0
  12. package/dist/server/router.js +126 -0
  13. package/dist/server/router.js.map +1 -0
  14. package/dist/server/schema.d.ts +1030 -0
  15. package/dist/server/schema.d.ts.map +1 -0
  16. package/dist/server/schema.js +137 -0
  17. package/dist/server/schema.js.map +1 -0
  18. package/dist/server/services/entitlements.d.ts +3 -0
  19. package/dist/server/services/entitlements.d.ts.map +1 -0
  20. package/dist/server/services/entitlements.js +68 -0
  21. package/dist/server/services/entitlements.js.map +1 -0
  22. package/dist/server/services/plans.d.ts +20 -0
  23. package/dist/server/services/plans.d.ts.map +1 -0
  24. package/dist/server/services/plans.js +122 -0
  25. package/dist/server/services/plans.js.map +1 -0
  26. package/dist/server/services/stripe.d.ts +42 -0
  27. package/dist/server/services/stripe.d.ts.map +1 -0
  28. package/dist/server/services/stripe.js +267 -0
  29. package/dist/server/services/stripe.js.map +1 -0
  30. package/dist/server/services/subscriptions.d.ts +35 -0
  31. package/dist/server/services/subscriptions.d.ts.map +1 -0
  32. package/dist/server/services/subscriptions.js +193 -0
  33. package/dist/server/services/subscriptions.js.map +1 -0
  34. package/dist/server/services/usage.d.ts +40 -0
  35. package/dist/server/services/usage.d.ts.map +1 -0
  36. package/dist/server/services/usage.js +98 -0
  37. package/dist/server/services/usage.js.map +1 -0
  38. package/migrations/0000_init.sql +82 -0
  39. package/migrations/0001_rls.sql +18 -0
  40. package/migrations/meta/0000_snapshot.json +580 -0
  41. package/migrations/meta/_journal.json +20 -0
  42. package/package.json +73 -0
  43. package/src/client/api.ts +15 -0
  44. package/src/client/format.test.ts +64 -0
  45. package/src/client/index.ts +81 -0
  46. package/src/contract.ts +311 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,MAAM,uDAA0B,CAAA;AAO7C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBjB,CAAA;AAED,gFAAgF;AAChF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqBzB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAOzB,CAAA;AAEF,kGAAkG;AAClG,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAOpB,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoBpB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQzB,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,uBAAwB,CAAA"}
@@ -0,0 +1,137 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { bigint, boolean, index, integer, jsonb, pgSchema, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
3
+ // `pgSchema` directly (not `moduleSchema` from @kernhq/kernel) so drizzle-kit can load this file standalone
4
+ export const schema = pgSchema('mod_billing');
5
+ const ts = (name) => timestamp(name, { withTimezone: true });
6
+ const id = () => uuid('id').primaryKey().default(sql `uuidv7()`);
7
+ const jsonObject = (name) => jsonb(name).notNull().default(sql `'{}'::jsonb`);
8
+ const jsonArray = (name) => jsonb(name).notNull().default(sql `'[]'::jsonb`);
9
+ /**
10
+ * # Why most of this module is not row-level secured
11
+ *
12
+ * Kern's rule is that a module's tenant tables carry `workspace_id` and RLS. Billing is the one place
13
+ * that rule does not fit, and it is worth saying exactly why rather than leaving the next person to
14
+ * assume it was forgotten.
15
+ *
16
+ * A subscription row is not the workspace's data. It is the **instance operator's** record *about*
17
+ * that workspace: what they charge it, whether it has paid, whether it is suspended. Two things
18
+ * follow, and neither is possible under RLS, which returns nothing at all when `app.workspace_id` is
19
+ * unset:
20
+ *
21
+ * - the instance console lists every workspace on the instance in one query — that screen is the
22
+ * whole reason an operator can run Kern as a service;
23
+ * - the dunning and reconcile jobs enumerate workspaces before they can pick one.
24
+ *
25
+ * So `plans`, `subscriptions`, `workspace_usage`, `overrides` and `webhook_events` are operator
26
+ * tables, and their isolation is enforced in the procedure layer: every workspace-facing procedure
27
+ * filters by the caller's own workspace and checks `billing.subscription.view|manage` first.
28
+ *
29
+ * `invoices` is the exception that proves it. An invoice **is** the customer's own record, read on
30
+ * their own billing screen, so it is a proper tenant table with RLS — see `TENANT_TABLES` below.
31
+ */
32
+ /** The catalogue. Instance-scoped: a plan belongs to the instance, not to any workspace. */
33
+ export const plans = schema.table('plans', {
34
+ id: id(),
35
+ slug: text('slug').notNull(),
36
+ name: text('name').notNull(),
37
+ description: text('description').notNull().default(''),
38
+ /** smallest currency unit, so 800 is $8.00 — money is never a float */
39
+ priceMinor: integer('price_minor').notNull().default(0),
40
+ currency: text('currency').notNull().default('usd'),
41
+ interval: text('interval').notNull().default('month'),
42
+ perSeat: boolean('per_seat').notNull().default(true),
43
+ trialDays: integer('trial_days').notNull().default(0),
44
+ limits: jsonObject('limits'),
45
+ stripePriceId: text('stripe_price_id'),
46
+ highlights: jsonArray('highlights'),
47
+ published: boolean('published').notNull().default(false),
48
+ order: integer('order').notNull().default(100),
49
+ /** archived plans keep working for whoever is on them, and stop being offered */
50
+ archivedAt: ts('archived_at'),
51
+ createdAt: ts('created_at').notNull().defaultNow(),
52
+ updatedAt: ts('updated_at').notNull().defaultNow(),
53
+ }, (t) => [uniqueIndex('plans_slug_idx').on(t.slug), index('plans_published_idx').on(t.published, t.order)]);
54
+ /** One per workspace. Operator table — see the note at the top of this file. */
55
+ export const subscriptions = schema.table('subscriptions', {
56
+ workspaceId: uuid('workspace_id').primaryKey(),
57
+ planId: uuid('plan_id'),
58
+ status: text('status').notNull().default('trialing'),
59
+ seatsPurchased: integer('seats_purchased').notNull().default(0),
60
+ trialEndsAt: ts('trial_ends_at'),
61
+ currentPeriodEnd: ts('current_period_end'),
62
+ cancelAtPeriodEnd: boolean('cancel_at_period_end').notNull().default(false),
63
+ stripeCustomerId: text('stripe_customer_id'),
64
+ stripeSubscriptionId: text('stripe_subscription_id'),
65
+ /** when the grace period after a failed payment runs out and the workspace is suspended */
66
+ graceEndsAt: ts('grace_ends_at'),
67
+ createdAt: ts('created_at').notNull().defaultNow(),
68
+ updatedAt: ts('updated_at').notNull().defaultNow(),
69
+ }, (t) => [
70
+ index('subscriptions_status_idx').on(t.status),
71
+ uniqueIndex('subscriptions_stripe_sub_idx').on(t.stripeSubscriptionId),
72
+ ]);
73
+ /**
74
+ * What a workspace is actually using.
75
+ *
76
+ * Kept as a counter rather than recomputed, because `sum(files.size)` on every upload is a scan of
77
+ * the workspace's whole file table. Event subscriptions move it; a nightly job recomputes the truth
78
+ * and logs the drift rather than silently papering over it, because silent correction hides the bug
79
+ * that caused the drift.
80
+ */
81
+ export const workspaceUsage = schema.table('workspace_usage', {
82
+ workspaceId: uuid('workspace_id').primaryKey(),
83
+ seats: integer('seats').notNull().default(0),
84
+ /** bigint, not integer: a 2 GiB workspace would overflow int4 counted in bytes */
85
+ storageBytes: bigint('storage_bytes', { mode: 'number' }).notNull().default(0),
86
+ reconciledAt: ts('reconciled_at'),
87
+ updatedAt: ts('updated_at').notNull().defaultNow(),
88
+ });
89
+ /** Per-workspace limit overrides, so an account can be comped without inventing a plan for it. */
90
+ export const overrides = schema.table('overrides', {
91
+ workspaceId: uuid('workspace_id').primaryKey(),
92
+ limits: jsonObject('limits'),
93
+ note: text('note').notNull().default(''),
94
+ createdBy: uuid('created_by'),
95
+ createdAt: ts('created_at').notNull().defaultNow(),
96
+ updatedAt: ts('updated_at').notNull().defaultNow(),
97
+ });
98
+ /**
99
+ * The customer's own invoice history — a **tenant table**, read on their billing screen in their own
100
+ * workspace context. The operator reads invoices in Stripe, which is authoritative; this is a mirror
101
+ * kept so the screen does not have to call Stripe to render.
102
+ */
103
+ export const invoices = schema.table('invoices', {
104
+ id: id(),
105
+ workspaceId: uuid('workspace_id').notNull(),
106
+ stripeInvoiceId: text('stripe_invoice_id'),
107
+ number: text('number'),
108
+ status: text('status').notNull(),
109
+ totalMinor: integer('total_minor').notNull().default(0),
110
+ currency: text('currency').notNull().default('usd'),
111
+ periodStart: ts('period_start'),
112
+ periodEnd: ts('period_end'),
113
+ hostedUrl: text('hosted_url'),
114
+ pdfUrl: text('pdf_url'),
115
+ createdAt: ts('created_at').notNull().defaultNow(),
116
+ }, (t) => [
117
+ index('invoices_ws_idx').on(t.workspaceId, t.createdAt),
118
+ uniqueIndex('invoices_stripe_idx').on(t.stripeInvoiceId),
119
+ ]);
120
+ /**
121
+ * Stripe event ids already applied.
122
+ *
123
+ * Stripe retries a webhook until it gets a 2xx, and it does not promise to deliver only once, so a
124
+ * handler that is not idempotent will double-count. The unique index is what makes the second
125
+ * delivery a no-op — checking then inserting is a race, so the insert *is* the check.
126
+ */
127
+ export const webhookEvents = schema.table('webhook_events', {
128
+ id: text('id').primaryKey(),
129
+ type: text('type').notNull(),
130
+ receivedAt: ts('received_at').notNull().defaultNow(),
131
+ }, (t) => [index('webhook_events_received_idx').on(t.receivedAt)]);
132
+ /**
133
+ * Tables that carry another workspace's rows and therefore must be row-level secured.
134
+ * Deliberately short — see the note at the top of this file for why the rest are operator tables.
135
+ */
136
+ export const TENANT_TABLES = ['invoices'];
137
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Entitlement, Kernel } from '@kernhq/kernel';
2
+ export declare function resolve(kernel: Kernel, workspaceId: string): Promise<Partial<Entitlement>>;
3
+ //# sourceMappingURL=entitlements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitlements.d.ts","sourceRoot":"","sources":["../../../src/server/services/entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AA8BzD,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAgDhG"}
@@ -0,0 +1,68 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { PlanLimits, PlanLimitsPatch } from '../../contract.js';
3
+ import { overrides, plans, subscriptions } from '../schema.js';
4
+ /**
5
+ * Statuses that still entitle a workspace to its plan.
6
+ *
7
+ * `past_due` is deliberately in the list: a card that failed this morning must not lock a team out
8
+ * of their work the same afternoon. The grace period is what ends, and ending it moves the row to
9
+ * `suspended` — which is not here, and which makes writes fail while reads keep working.
10
+ */
11
+ const ENTITLED = new Set(['trialing', 'active', 'past_due']);
12
+ /** Statuses where the workspace may read but not write. */
13
+ const INACTIVE = new Set(['suspended', 'canceled']);
14
+ /**
15
+ * What a workspace is allowed to do, resolved from its plan and any override an admin has set.
16
+ *
17
+ * The result is exactly the shape `kernel.entitlements` expects, because this is the procedure the
18
+ * kernel calls. Anything this function cannot answer — no subscription row, no plan, an unparseable
19
+ * limits blob — resolves to *unlimited* rather than to nothing. A billing bug must not be able to
20
+ * lock a paying customer out of their own workspace; the failure has to fall open.
21
+ */
22
+ /** Drop the keys a patch does not mention, so a spread cannot overwrite with `undefined`. */
23
+ function definedOnly(patch) {
24
+ return Object.fromEntries(Object.entries(patch).filter(([, v]) => v !== undefined));
25
+ }
26
+ export async function resolve(kernel, workspaceId) {
27
+ const db = kernel.database.db;
28
+ const [row] = await db
29
+ .select({
30
+ status: subscriptions.status,
31
+ planName: plans.name,
32
+ limits: plans.limits,
33
+ })
34
+ .from(subscriptions)
35
+ .leftJoin(plans, eq(plans.id, subscriptions.planId))
36
+ .where(eq(subscriptions.workspaceId, workspaceId))
37
+ .limit(1);
38
+ // Nothing bills this workspace — a self-hosted instance, or one where the admin has not assigned
39
+ // a plan. Unlimited, and silent about it.
40
+ if (!row)
41
+ return { planName: null, active: true };
42
+ const parsed = PlanLimits.safeParse(row.limits ?? {});
43
+ if (!parsed.success)
44
+ kernel.log.warn({ workspaceId, issues: parsed.error.issues.length }, 'billing: plan limits did not parse; treating the workspace as unlimited');
45
+ const base = parsed.success ? parsed.data : PlanLimits.parse({});
46
+ const [ovr] = await db
47
+ .select({ limits: overrides.limits })
48
+ .from(overrides)
49
+ .where(eq(overrides.workspaceId, workspaceId))
50
+ .limit(1);
51
+ // An override is a patch over the plan, so comping one limit does not silently reset the others to
52
+ // a default the admin never chose — which means dropping the keys the patch does not mention
53
+ // rather than spreading them as `undefined`.
54
+ const patch = ovr?.limits ? definedOnly(PlanLimitsPatch.parse(ovr.limits)) : null;
55
+ const merged = patch
56
+ ? { ...base, ...patch }
57
+ : ENTITLED.has(row.status)
58
+ ? base
59
+ : // Not entitled: the plan's allowances do not apply, but nothing is taken away that would
60
+ // stop the customer reading and exporting what is theirs.
61
+ PlanLimits.parse({});
62
+ return {
63
+ ...merged,
64
+ planName: row.planName ?? null,
65
+ active: !INACTIVE.has(row.status),
66
+ };
67
+ }
68
+ //# sourceMappingURL=entitlements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitlements.js","sourceRoot":"","sources":["../../../src/server/services/entitlements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,eAAe,EAA2B,MAAM,mBAAmB,CAAA;AACxF,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,QAAQ,GAAwB,IAAI,GAAG,CAAqB,CAAC,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAA;AAErG,2DAA2D;AAC3D,MAAM,QAAQ,GAAwB,IAAI,GAAG,CAAqB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAA;AAE5F;;;;;;;GAOG;AACH,6FAA6F;AAC7F,SAAS,WAAW,CAAmB,KAAQ;IAC7C,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAe,CAAA;AACnG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAc,EAAE,WAAmB;IAC/D,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAA;IAC7B,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;SACnB,MAAM,CAAC;QACN,MAAM,EAAE,aAAa,CAAC,MAAM;QAC5B,QAAQ,EAAE,KAAK,CAAC,IAAI;QACpB,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAC;SACD,IAAI,CAAC,aAAa,CAAC;SACnB,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;SACnD,KAAK,CAAC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;SACjD,KAAK,CAAC,CAAC,CAAC,CAAA;IAEX,iGAAiG;IACjG,0CAA0C;IAC1C,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IAEjD,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;IACrD,IAAI,CAAC,MAAM,CAAC,OAAO;QACjB,MAAM,CAAC,GAAG,CAAC,IAAI,CACb,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,EACnD,yEAAyE,CAC1E,CAAA;IACH,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAEhE,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;SACnB,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;SACpC,IAAI,CAAC,SAAS,CAAC;SACf,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;SAC7C,KAAK,CAAC,CAAC,CAAC,CAAA;IAEX,mGAAmG;IACnG,6FAA6F;IAC7F,6CAA6C;IAC7C,MAAM,KAAK,GAAG,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACjF,MAAM,MAAM,GAAG,KAAK;QAClB,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,EAAE;QACvB,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;YACxB,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,yFAAyF;gBACzF,0DAA0D;gBAC1D,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAE1B,OAAO;QACL,GAAG,MAAM;QACT,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,IAAI;QAC9B,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;KAClC,CAAA;AACH,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { type Kernel } from '@kernhq/kernel';
2
+ import { type Plan, type PublicPlan, type UpsertPlan } from '../../contract.js';
3
+ import { plans } from '../schema.js';
4
+ type Row = typeof plans.$inferSelect;
5
+ declare const ser: (r: Row) => Plan;
6
+ /** What a stranger sees: published, unarchived, and nothing internal. */
7
+ declare const serPublic: (r: Row) => PublicPlan;
8
+ export declare function list(kernel: Kernel, includeUnpublished: boolean): Promise<Plan[]>;
9
+ export declare function publicList(kernel: Kernel): Promise<PublicPlan[]>;
10
+ export declare function bySlug(kernel: Kernel, slug: string): Promise<Plan | null>;
11
+ export declare function upsert(kernel: Kernel, input: UpsertPlan): Promise<Plan>;
12
+ export declare function setPublished(kernel: Kernel, id: string, published: boolean): Promise<Plan>;
13
+ /**
14
+ * Archiving withdraws a plan from the catalogue without deleting it: whoever is already on it stays
15
+ * on it, and their subscription row keeps pointing at something that still resolves. Deleting the
16
+ * row would leave those workspaces with a dangling `plan_id` and no limits at all.
17
+ */
18
+ export declare function archive(kernel: Kernel, id: string): Promise<void>;
19
+ export { ser as serialisePlan, serPublic as serialisePublicPlan };
20
+ //# sourceMappingURL=plans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../../src/server/services/plans.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,MAAM,EAAU,MAAM,gBAAgB,CAAA;AAE/D,OAAO,EAAE,KAAK,IAAI,EAAc,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC3F,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAEpC,KAAK,GAAG,GAAG,OAAO,KAAK,CAAC,YAAY,CAAA;AAEpC,QAAA,MAAM,GAAG,GAAI,GAAG,GAAG,KAAG,IAiBpB,CAAA;AAEF,yEAAyE;AACzE,QAAA,MAAM,SAAS,GAAI,GAAG,GAAG,KAAG,UAY1B,CAAA;AAEF,wBAAsB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAUvF;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAOtE;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAG/E;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC7E;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhG;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvE;AAED,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,SAAS,IAAI,mBAAmB,EAAE,CAAA"}
@@ -0,0 +1,122 @@
1
+ import { KernError, uuidv7 } from '@kernhq/kernel';
2
+ import { and, asc, eq, isNull, ne } from 'drizzle-orm';
3
+ import { PlanLimits } from '../../contract.js';
4
+ import { plans } from '../schema.js';
5
+ const ser = (r) => ({
6
+ id: r.id,
7
+ slug: r.slug,
8
+ name: r.name,
9
+ description: r.description,
10
+ priceMinor: r.priceMinor,
11
+ currency: r.currency,
12
+ interval: r.interval,
13
+ perSeat: r.perSeat,
14
+ trialDays: r.trialDays,
15
+ limits: PlanLimits.parse(r.limits ?? {}),
16
+ stripePriceId: r.stripePriceId,
17
+ highlights: r.highlights ?? [],
18
+ published: r.published,
19
+ order: r.order,
20
+ createdAt: r.createdAt.toISOString(),
21
+ updatedAt: r.updatedAt.toISOString(),
22
+ });
23
+ /** What a stranger sees: published, unarchived, and nothing internal. */
24
+ const serPublic = (r) => ({
25
+ slug: r.slug,
26
+ name: r.name,
27
+ description: r.description,
28
+ priceMinor: r.priceMinor,
29
+ currency: r.currency,
30
+ interval: r.interval,
31
+ perSeat: r.perSeat,
32
+ trialDays: r.trialDays,
33
+ highlights: r.highlights ?? [],
34
+ order: r.order,
35
+ limits: PlanLimits.parse(r.limits ?? {}),
36
+ });
37
+ export async function list(kernel, includeUnpublished) {
38
+ const where = includeUnpublished
39
+ ? isNull(plans.archivedAt)
40
+ : and(isNull(plans.archivedAt), eq(plans.published, true));
41
+ const rows = await kernel.database.db
42
+ .select()
43
+ .from(plans)
44
+ .where(where)
45
+ .orderBy(asc(plans.order), asc(plans.priceMinor));
46
+ return rows.map(ser);
47
+ }
48
+ export async function publicList(kernel) {
49
+ const rows = await kernel.database.db
50
+ .select()
51
+ .from(plans)
52
+ .where(and(isNull(plans.archivedAt), eq(plans.published, true)))
53
+ .orderBy(asc(plans.order), asc(plans.priceMinor));
54
+ return rows.map(serPublic);
55
+ }
56
+ export async function bySlug(kernel, slug) {
57
+ const [row] = await kernel.database.db.select().from(plans).where(eq(plans.slug, slug)).limit(1);
58
+ return row ? ser(row) : null;
59
+ }
60
+ export async function upsert(kernel, input) {
61
+ const db = kernel.database.db;
62
+ // A slug appears in URLs and in whatever the marketing site has cached, so two plans may never
63
+ // share one — checked explicitly so the admin gets a sentence rather than a unique-violation.
64
+ const [clash] = await db
65
+ .select({ id: plans.id })
66
+ .from(plans)
67
+ .where(input.id ? and(eq(plans.slug, input.slug), ne(plans.id, input.id)) : eq(plans.slug, input.slug))
68
+ .limit(1);
69
+ if (clash)
70
+ throw KernError.conflict('Another plan already uses that slug', 'billing.plan.slug_taken');
71
+ const values = {
72
+ slug: input.slug,
73
+ name: input.name,
74
+ description: input.description,
75
+ priceMinor: input.priceMinor,
76
+ currency: input.currency,
77
+ interval: input.interval,
78
+ perSeat: input.perSeat,
79
+ trialDays: input.trialDays,
80
+ limits: PlanLimits.parse(input.limits),
81
+ stripePriceId: input.stripePriceId,
82
+ highlights: input.highlights,
83
+ published: input.published,
84
+ order: input.order,
85
+ updatedAt: new Date(),
86
+ };
87
+ const [row] = input.id
88
+ ? await db.update(plans).set(values).where(eq(plans.id, input.id)).returning()
89
+ : await db
90
+ .insert(plans)
91
+ .values({ id: uuidv7(), ...values })
92
+ .returning();
93
+ if (!row)
94
+ throw KernError.notFound('Plan');
95
+ return ser(row);
96
+ }
97
+ export async function setPublished(kernel, id, published) {
98
+ const [row] = await kernel.database.db
99
+ .update(plans)
100
+ .set({ published, updatedAt: new Date() })
101
+ .where(eq(plans.id, id))
102
+ .returning();
103
+ if (!row)
104
+ throw KernError.notFound('Plan');
105
+ return ser(row);
106
+ }
107
+ /**
108
+ * Archiving withdraws a plan from the catalogue without deleting it: whoever is already on it stays
109
+ * on it, and their subscription row keeps pointing at something that still resolves. Deleting the
110
+ * row would leave those workspaces with a dangling `plan_id` and no limits at all.
111
+ */
112
+ export async function archive(kernel, id) {
113
+ const [row] = await kernel.database.db
114
+ .update(plans)
115
+ .set({ archivedAt: new Date(), published: false, updatedAt: new Date() })
116
+ .where(eq(plans.id, id))
117
+ .returning({ id: plans.id });
118
+ if (!row)
119
+ throw KernError.notFound('Plan');
120
+ }
121
+ export { ser as serialisePlan, serPublic as serialisePublicPlan };
122
+ //# sourceMappingURL=plans.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plans.js","sourceRoot":"","sources":["../../../src/server/services/plans.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { type Kernel } from '@kernhq/kernel';
2
+ import Stripe from 'stripe';
3
+ /**
4
+ * Stripe, or nothing at all.
5
+ *
6
+ * The module ships in every Kern image, including every self-hosted one, so the absence of a key is
7
+ * the normal case rather than a misconfiguration. `client()` returning null is how the rest of the
8
+ * module finds out, and every caller has to handle it — there is no "assume it is configured" path.
9
+ */
10
+ export declare function client(): Stripe | null;
11
+ export declare const paymentsEnabled: () => boolean;
12
+ export declare function checkout(kernel: Kernel, input: {
13
+ workspaceId: string;
14
+ planSlug: string;
15
+ seats?: number;
16
+ email?: string;
17
+ baseUrl: string;
18
+ }): Promise<{
19
+ url: string;
20
+ }>;
21
+ export declare function portal(kernel: Kernel, input: {
22
+ workspaceId: string;
23
+ returnUrl: string;
24
+ }): Promise<{
25
+ url: string;
26
+ }>;
27
+ /** Keep the seat quantity on Stripe in step with the workspace's actual membership. */
28
+ export declare function syncSeats(kernel: Kernel, workspaceId: string, seats: number): Promise<void>;
29
+ /**
30
+ * Apply one webhook.
31
+ *
32
+ * Verification happens against the **raw body**, before anything parses it — a signature over
33
+ * re-encoded JSON proves nothing, because re-encoding is not guaranteed to reproduce the bytes that
34
+ * were signed.
35
+ */
36
+ export declare function handleWebhook(kernel: Kernel, raw: Buffer | string, signature: string): Promise<{
37
+ handled: boolean;
38
+ type: string;
39
+ }>;
40
+ /** How long a workspace keeps working after a payment fails. */
41
+ export declare const GRACE_DAYS = 14;
42
+ //# sourceMappingURL=stripe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe.d.ts","sourceRoot":"","sources":["../../../src/server/services/stripe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvD,OAAO,MAAM,MAAM,QAAQ,CAAA;AAK3B;;;;;;GAMG;AACH,wBAAgB,MAAM,IAAI,MAAM,GAAG,IAAI,CAUtC;AAED,eAAO,MAAM,eAAe,eAA+C,CAAA;AAiD3E,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAuB1B;AAED,wBAAsB,MAAM,CAC1B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAChD,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAU1B;AAED,uFAAuF;AACvF,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjG;AAkFD;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GAAG,MAAM,EACpB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAyD7C;AAED,gEAAgE;AAChE,eAAO,MAAM,UAAU,KAAK,CAAA"}