nomkit 0.0.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/LICENSE.txt +21 -0
- package/dist/_virtual/_rolldown/runtime.js +27 -0
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/index.js +6 -0
- package/dist/cli/commands/push.d.ts +6 -0
- package/dist/cli/commands/push.js +143 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/lib/collection_sync.d.ts +107 -0
- package/dist/cli/lib/collection_sync.js +158 -0
- package/dist/cli/lib/config_loader.d.ts +15 -0
- package/dist/cli/lib/config_loader.js +43 -0
- package/dist/cli/lib/hash.d.ts +22 -0
- package/dist/cli/lib/hash.js +63 -0
- package/dist/cli/lib/migrations.d.ts +6 -0
- package/dist/cli/lib/migrations.js +17 -0
- package/dist/client/index.d.ts +13 -0
- package/dist/client/index.js +34 -0
- package/dist/core/nomba_api/banks.d.ts +14 -0
- package/dist/core/nomba_api/banks.js +0 -0
- package/dist/core/nomba_api/charge-tokenized-card.d.ts +33 -0
- package/dist/core/nomba_api/charge-tokenized-card.js +0 -0
- package/dist/core/nomba_api/checkout.d.ts +44 -0
- package/dist/core/nomba_api/checkout.js +0 -0
- package/dist/core/nomba_api/get_checkout.d.ts +57 -0
- package/dist/core/nomba_api/get_checkout.js +0 -0
- package/dist/core/nomba_api/index.d.ts +313 -0
- package/dist/core/nomba_api/index.js +179 -0
- package/dist/core/nomba_api/lib/utils.d.ts +235 -0
- package/dist/core/nomba_api/lib/utils.js +313 -0
- package/dist/core/nomba_api/list-tokenized-cards.d.ts +24 -0
- package/dist/core/nomba_api/list-tokenized-cards.js +0 -0
- package/dist/core/nomba_api/token-manager/index.d.ts +51 -0
- package/dist/core/nomba_api/token-manager/index.js +109 -0
- package/dist/core/pg_db/index.d.ts +108 -0
- package/dist/core/pg_db/index.js +76 -0
- package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/migration.sql +120 -0
- package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/snapshot.json +1616 -0
- package/dist/core/pg_db/relations.d.ts +46 -0
- package/dist/core/pg_db/relations.js +83 -0
- package/dist/core/pg_db/schema.d.ts +1138 -0
- package/dist/core/pg_db/schema.js +124 -0
- package/dist/endpoints/customers/api.js +51 -0
- package/dist/endpoints/entitlements/api.js +42 -0
- package/dist/endpoints/routes.d.ts +15 -0
- package/dist/endpoints/routes.js +15 -0
- package/dist/endpoints/subscriptions/api.js +263 -0
- package/dist/endpoints/subscriptions/utils.js +105 -0
- package/dist/endpoints/webhooks/invoice/api.js +28 -0
- package/dist/endpoints/webhooks/nomba/api.js +76 -0
- package/dist/endpoints/webhooks/nomba/utils.js +36 -0
- package/dist/index.d.ts +204 -0
- package/dist/index.js +175 -0
- package/dist/lib/utils.d.ts +21 -0
- package/dist/lib/utils.js +41 -0
- package/dist/node_modules/.pnpm/@better-fetch_fetch@1.3.1/node_modules/@better-fetch/fetch/dist/index.js +475 -0
- package/dist/package.js +4 -0
- package/dist/queue/backends/pglite/backend.d.ts +43 -0
- package/dist/queue/backends/pglite/backend.js +33 -0
- package/dist/queue/backends/pglite/index.d.ts +4 -0
- package/dist/queue/backends/pglite/index.js +4 -0
- package/dist/queue/backends/pglite/migrations/schema.d.ts +4 -0
- package/dist/queue/backends/pglite/migrations/schema.js +37 -0
- package/dist/queue/backends/pglite/notification-channel.d.ts +17 -0
- package/dist/queue/backends/pglite/notification-channel.js +61 -0
- package/dist/queue/backends/pglite/repository.d.ts +38 -0
- package/dist/queue/backends/pglite/repository.js +299 -0
- package/dist/queue/backends/redis/index.d.ts +7 -0
- package/dist/queue/backends/redis/index.js +1 -0
- package/dist/queue/client/index.d.ts +12 -0
- package/dist/queue/client/index.js +31 -0
- package/dist/queue/endpoints/api.d.ts +53 -0
- package/dist/queue/endpoints/api.js +45 -0
- package/dist/queue/endpoints/routes.d.ts +32 -0
- package/dist/queue/endpoints/routes.js +5 -0
- package/dist/queue/init.d.ts +27 -0
- package/dist/queue/init.js +31 -0
- package/dist/queue/lib/billing.d.ts +25 -0
- package/dist/queue/lib/billing.js +87 -0
- package/dist/queue/lib/utils.d.ts +30 -0
- package/dist/queue/lib/utils.js +35 -0
- package/package.json +71 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { __exportAll } from "../../../_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { subscriptions } from "../../../core/pg_db/schema.js";
|
|
3
|
+
import { defineNomkitMethod } from "../../../lib/utils.js";
|
|
4
|
+
import { getPaymentInterval, isValidSignature } from "./utils.js";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
import { eq } from "drizzle-orm";
|
|
7
|
+
//#region endpoints/webhooks/nomba/api.ts
|
|
8
|
+
var api_exports = /* @__PURE__ */ __exportAll({ nombaWebhook: () => nombaWebhook });
|
|
9
|
+
const nombaWebhook = defineNomkitMethod({
|
|
10
|
+
input: z.object({ event_type: z.enum(["payment_success", "payment_failed"]) }),
|
|
11
|
+
route: {
|
|
12
|
+
path: "/webhooks/nomba",
|
|
13
|
+
clientOnly: true,
|
|
14
|
+
requireHeaders: true
|
|
15
|
+
}
|
|
16
|
+
}, async (ctx) => {
|
|
17
|
+
const db = ctx.nomkit.database;
|
|
18
|
+
const ev = ctx.input;
|
|
19
|
+
const meta = ev.data?.order?.orderMetaData;
|
|
20
|
+
if (!meta) return ctx.json({ msg: "Invalid event metadata" }, { status: 404 });
|
|
21
|
+
if (!isValidSignature({
|
|
22
|
+
headers: ctx.headers,
|
|
23
|
+
ev,
|
|
24
|
+
nomkit: ctx.nomkit
|
|
25
|
+
})) return ctx.json({ msg: "Invalid webhook payload bad signature" }, { status: 404 });
|
|
26
|
+
const subscriptionId = meta["nomkit.subscription.id"];
|
|
27
|
+
const previousSubscriptionId = meta["nomkit.subscription.previous_subscription_id"];
|
|
28
|
+
const kind = meta["nomkit.subscription.kind"];
|
|
29
|
+
const subscription = await db.query.subscriptions.findFirst({ where: { id: subscriptionId } });
|
|
30
|
+
if (!subscription) {
|
|
31
|
+
console.log("Webhook for unknown subscription");
|
|
32
|
+
return ctx.json({ msg: "Unknown webhook subscription" }, { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
if (subscription.customerId !== meta["nomkit.subscription.customer_id"]) {
|
|
35
|
+
console.log("Webhook metadata customer mismatch");
|
|
36
|
+
return ctx.json({ msg: "Webhook metadata customer mismatch" }, { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
if (subscription.status === "active") {
|
|
39
|
+
console.log("Already processed");
|
|
40
|
+
return ctx.json({ msg: "Already processed" });
|
|
41
|
+
}
|
|
42
|
+
if (ev.event_type === "payment_failed") {
|
|
43
|
+
console.log("Payment failed, subscription canceled");
|
|
44
|
+
await db.update(subscriptions).set({
|
|
45
|
+
status: "cancelled",
|
|
46
|
+
endedAt: /* @__PURE__ */ new Date()
|
|
47
|
+
}).where(eq(subscriptions.id, subscription.id));
|
|
48
|
+
return ctx.json({ msg: "Payment failed, subscription canceled" });
|
|
49
|
+
}
|
|
50
|
+
const plan = await db.query.products.findFirst({ where: { internalId: subscription.productInternalId } });
|
|
51
|
+
if (!plan) {
|
|
52
|
+
console.log("Subscription references missing product");
|
|
53
|
+
return ctx.json({ msg: "Subscription references missing product" });
|
|
54
|
+
}
|
|
55
|
+
const paidAt = ev.data.transaction.time;
|
|
56
|
+
const { periodStart, periodEnd } = getPaymentInterval({
|
|
57
|
+
paidAt,
|
|
58
|
+
interval: plan.priceInterval
|
|
59
|
+
});
|
|
60
|
+
await db.update(subscriptions).set({
|
|
61
|
+
status: "active",
|
|
62
|
+
currentPeriodStart: new Date(periodStart.epochMilliseconds),
|
|
63
|
+
currentPeriodEnd: new Date(periodEnd.epochMilliseconds)
|
|
64
|
+
}).where(eq(subscriptions.id, subscription.id));
|
|
65
|
+
if ((kind === "upgrade" || kind === "downgrade") && previousSubscriptionId) {
|
|
66
|
+
const previousSubscription = await db.query.subscriptions.findFirst({ where: { id: previousSubscriptionId } });
|
|
67
|
+
if (!previousSubscription) console.log("Previous subscription not found, skipping cancel");
|
|
68
|
+
else if (previousSubscription.status !== "cancelled") await db.update(subscriptions).set({
|
|
69
|
+
status: "cancelled",
|
|
70
|
+
endedAt: /* @__PURE__ */ new Date()
|
|
71
|
+
}).where(eq(subscriptions.id, previousSubscription.id));
|
|
72
|
+
}
|
|
73
|
+
return ctx.json({ msg: "Webhook processed" });
|
|
74
|
+
});
|
|
75
|
+
//#endregion
|
|
76
|
+
export { api_exports, nombaWebhook };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { hashPayload } from "../../../core/nomba_api/index.js";
|
|
2
|
+
import ms from "ms";
|
|
3
|
+
import { Temporal } from "temporal-polyfill";
|
|
4
|
+
//#region endpoints/webhooks/nomba/utils.ts
|
|
5
|
+
async function isValidSignature(args) {
|
|
6
|
+
const headers = args.headers;
|
|
7
|
+
const signature = headers.get("nomba-sig-value") ?? headers.get("nomba-signature");
|
|
8
|
+
const timestamp = headers.get("nomba-timestamp");
|
|
9
|
+
if (!signature) {
|
|
10
|
+
console.log("There is no nomba-sig-value header");
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (!timestamp) {
|
|
14
|
+
console.log("There is no nomba-timestamp header");
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const payload = args.ev;
|
|
18
|
+
const hash = await hashPayload({
|
|
19
|
+
payload,
|
|
20
|
+
secretKey: args.nomkit.nomba.getWebhookSecret(),
|
|
21
|
+
timestamp
|
|
22
|
+
});
|
|
23
|
+
return signature.toLowerCase() !== hash.toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
function getPaymentInterval(args) {
|
|
26
|
+
const periodStart = Temporal.Instant.from(args.paidAt);
|
|
27
|
+
const milliseconds = ms(args.interval);
|
|
28
|
+
if (typeof milliseconds !== "number" || Number.isNaN(milliseconds)) throw new Error("You have an invalid interval");
|
|
29
|
+
if (milliseconds < 0) throw new Error("timestamp cannot include negative intervals");
|
|
30
|
+
return {
|
|
31
|
+
periodStart,
|
|
32
|
+
periodEnd: periodStart.add(Temporal.Duration.from({ milliseconds }))
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { getPaymentInterval, isValidSignature };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { NombaAPI } from "./core/nomba_api/index.js";
|
|
2
|
+
import { DatabaseConfig } from "./adapters/index.js";
|
|
3
|
+
import { Customer } from "./core/pg_db/schema.js";
|
|
4
|
+
import { routes } from "./queue/endpoints/routes.js";
|
|
5
|
+
import { InferRouteAPI } from "./queue/client/index.js";
|
|
6
|
+
import { StringValue } from "ms";
|
|
7
|
+
|
|
8
|
+
//#region index.d.ts
|
|
9
|
+
declare const DAYLY_INTERVAL: unique symbol;
|
|
10
|
+
declare const MONTHLY_INTERVAL: unique symbol;
|
|
11
|
+
declare const ANNUALLY_INTERVAL: unique symbol;
|
|
12
|
+
declare const Interval: {
|
|
13
|
+
/** Defaults to `"24 hours"` */readonly DAYLY: typeof DAYLY_INTERVAL; /** Approx to `"28 days"` every cycle */
|
|
14
|
+
readonly MONTHLY: typeof MONTHLY_INTERVAL; /** Period of `"1 year"` every cycle */
|
|
15
|
+
readonly ANNUALLY: typeof ANNUALLY_INTERVAL;
|
|
16
|
+
readonly custom: <T extends StringValue>(every: T) => {
|
|
17
|
+
interval: "custom";
|
|
18
|
+
every: T;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
type BuiltinInterval = Exclude<(typeof Interval)[keyof typeof Interval], typeof Interval.custom>;
|
|
22
|
+
type CustomInterval = ReturnType<typeof Interval.custom>;
|
|
23
|
+
type Interval = BuiltinInterval | CustomInterval;
|
|
24
|
+
type Retries<T extends string = string> = {
|
|
25
|
+
interval: "custom";
|
|
26
|
+
after: T;
|
|
27
|
+
};
|
|
28
|
+
declare const Retry: {
|
|
29
|
+
readonly after: <const T extends StringValue>(after: T) => Retries<T>;
|
|
30
|
+
};
|
|
31
|
+
type Prettify<T> = { -readonly [K in keyof T]: T[K] } & {};
|
|
32
|
+
interface FeatureOpts<T extends string = string> {
|
|
33
|
+
id: T;
|
|
34
|
+
label: string;
|
|
35
|
+
type: "boolean" | "metered";
|
|
36
|
+
}
|
|
37
|
+
type MeteredOpts = {
|
|
38
|
+
limit: number;
|
|
39
|
+
reset: "week" | "month" | "year";
|
|
40
|
+
};
|
|
41
|
+
type BooleanFeature<L> = () => Prettify<L>;
|
|
42
|
+
type MeteredFeature<L> = <const MFeature extends MeteredOpts>(args: MFeature) => Prettify<L & MFeature>;
|
|
43
|
+
type BooleanFeatureOpts<T extends string = string> = FeatureOpts<T> & {
|
|
44
|
+
type: "boolean";
|
|
45
|
+
};
|
|
46
|
+
type MeteredFeatureOpts<T extends string = string> = FeatureOpts<T> & {
|
|
47
|
+
type: "metered";
|
|
48
|
+
};
|
|
49
|
+
declare function feature<const TFeature extends BooleanFeatureOpts>(args: TFeature): BooleanFeature<TFeature>;
|
|
50
|
+
declare function feature<const TFeature extends MeteredFeatureOpts>(args: TFeature): MeteredFeature<TFeature>;
|
|
51
|
+
type Feature = BooleanFeatureOpts | MeteredFeatureOpts;
|
|
52
|
+
interface PlanOptions<TIncludes extends readonly Feature[] = [], T extends string = string> {
|
|
53
|
+
id: T;
|
|
54
|
+
label: string;
|
|
55
|
+
includes?: TIncludes;
|
|
56
|
+
price?: {
|
|
57
|
+
/**
|
|
58
|
+
* Amount in naira
|
|
59
|
+
*/
|
|
60
|
+
amount: number;
|
|
61
|
+
interval: Interval;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
type Plan<TIncludes extends readonly Feature[] = [], T extends string = string> = Omit<PlanOptions<TIncludes, T>, "price"> & {
|
|
65
|
+
price?: {
|
|
66
|
+
amount: number;
|
|
67
|
+
interval: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
declare function plan<TIncludes extends readonly Feature[] = [], const T extends string = string>(options: PlanOptions<TIncludes, T>): Plan<TIncludes, T>;
|
|
71
|
+
type SubscribeOptions<TPlanId extends string> = {
|
|
72
|
+
customerId: string;
|
|
73
|
+
planID: TPlanId;
|
|
74
|
+
successURL: string;
|
|
75
|
+
cancelURL?: string;
|
|
76
|
+
};
|
|
77
|
+
type SubscribeResult = {
|
|
78
|
+
checkoutURL: string | null;
|
|
79
|
+
};
|
|
80
|
+
type CheckOptions<TFeatureId extends string> = {
|
|
81
|
+
customerId: string;
|
|
82
|
+
featureId: TFeatureId;
|
|
83
|
+
};
|
|
84
|
+
type CheckResult<T> = T extends {
|
|
85
|
+
type: "metered";
|
|
86
|
+
} ? {
|
|
87
|
+
allowed: boolean;
|
|
88
|
+
balance: {
|
|
89
|
+
limit: number;
|
|
90
|
+
remaining: number;
|
|
91
|
+
resetAt: string;
|
|
92
|
+
unlimited: boolean;
|
|
93
|
+
};
|
|
94
|
+
} : T extends {
|
|
95
|
+
type: "boolean";
|
|
96
|
+
} ? {
|
|
97
|
+
allowed: boolean;
|
|
98
|
+
} : never;
|
|
99
|
+
type ConsumeOptions<TFeatureId> = {
|
|
100
|
+
customerId: string;
|
|
101
|
+
featureId: TFeatureId;
|
|
102
|
+
amount: number;
|
|
103
|
+
};
|
|
104
|
+
type ConsumeResult<TFeatureId> = {
|
|
105
|
+
featureId: TFeatureId;
|
|
106
|
+
balance: number;
|
|
107
|
+
limit: number;
|
|
108
|
+
unlimited?: boolean;
|
|
109
|
+
};
|
|
110
|
+
type GetCustomer = {
|
|
111
|
+
customerId?: string;
|
|
112
|
+
externalId?: string;
|
|
113
|
+
};
|
|
114
|
+
type GetCustomerResult = Customer | null;
|
|
115
|
+
type UpsertCustomer = {
|
|
116
|
+
name?: string;
|
|
117
|
+
externalId?: string;
|
|
118
|
+
email: string;
|
|
119
|
+
metadata: Record<string, string> | null;
|
|
120
|
+
};
|
|
121
|
+
type ListCustomers = {
|
|
122
|
+
cursor?: string;
|
|
123
|
+
limit: number;
|
|
124
|
+
};
|
|
125
|
+
type ListCustomersResult = Customer[];
|
|
126
|
+
type DeleteCustomer = {
|
|
127
|
+
customerId: string;
|
|
128
|
+
};
|
|
129
|
+
type InferBooleanFeatures<T> = T extends {
|
|
130
|
+
id: infer X;
|
|
131
|
+
type: "boolean";
|
|
132
|
+
} ? X : never;
|
|
133
|
+
type InferMeteredFeatures<T> = T extends {
|
|
134
|
+
id: infer X;
|
|
135
|
+
type: "metered";
|
|
136
|
+
} ? X : never;
|
|
137
|
+
interface NombaProvider {
|
|
138
|
+
accountId: string;
|
|
139
|
+
clientId: string;
|
|
140
|
+
clientSecret: string;
|
|
141
|
+
webhookSecret: string;
|
|
142
|
+
}
|
|
143
|
+
type InferRetryPolicy<T extends string> = `after:${T}`;
|
|
144
|
+
type EventHandler<TArgs = unknown> = (args: TArgs) => void | Promise<void>;
|
|
145
|
+
type RetryEvents<T extends string> = Partial<Record<T, EventHandler<{
|
|
146
|
+
trigger: string;
|
|
147
|
+
customer: Customer;
|
|
148
|
+
}>>>;
|
|
149
|
+
type DefaultEvents<R extends string> = Partial<{
|
|
150
|
+
"customer.updated": EventHandler;
|
|
151
|
+
"after:*": EventHandler<{
|
|
152
|
+
trigger: R;
|
|
153
|
+
customer: Customer;
|
|
154
|
+
isStale: boolean;
|
|
155
|
+
}>;
|
|
156
|
+
}>;
|
|
157
|
+
type Event<T extends string = string> = DefaultEvents<T> & RetryEvents<InferRetryPolicy<T>>;
|
|
158
|
+
interface CreateNomKit<TRetries extends readonly Retries[], TProducts extends Plan<any>[] = Plan<any>[]> {
|
|
159
|
+
database: DatabaseConfig;
|
|
160
|
+
products: TProducts;
|
|
161
|
+
enableProrating?: boolean;
|
|
162
|
+
paymentProviders: {
|
|
163
|
+
nomba: NombaProvider;
|
|
164
|
+
};
|
|
165
|
+
queueConfig: {
|
|
166
|
+
baseURL: string;
|
|
167
|
+
};
|
|
168
|
+
retryPolicy: TRetries;
|
|
169
|
+
on?: Event<TRetries[number]["after"]>;
|
|
170
|
+
}
|
|
171
|
+
type NomKitOptions = CreateNomKit<Retries[], Plan[]>;
|
|
172
|
+
interface NomkitContext {
|
|
173
|
+
database: DatabaseConfig;
|
|
174
|
+
nomba: NombaAPI;
|
|
175
|
+
queueClient: InferRouteAPI<typeof routes>;
|
|
176
|
+
on?: Event<string>;
|
|
177
|
+
}
|
|
178
|
+
declare function createNomkit<const TRetries extends Retries[], const TProducts extends Plan<any>[]>(args: CreateNomKit<TRetries, TProducts>): {
|
|
179
|
+
options: CreateNomKit<TRetries, TProducts>;
|
|
180
|
+
api: {
|
|
181
|
+
subscribe(data: SubscribeOptions<TProducts[number]["id"]>): Promise<SubscribeResult | null>;
|
|
182
|
+
/**
|
|
183
|
+
* Entitlements represent what a customer can currently do based on
|
|
184
|
+
* their active plan.
|
|
185
|
+
*/
|
|
186
|
+
entitlements: {
|
|
187
|
+
check<T extends NonNullable<TProducts[number]["includes"]>[number]["id"]>(data: CheckOptions<T>): Promise<CheckResult<Extract<NonNullable<TProducts[number]["includes"]>[number], {
|
|
188
|
+
id: T;
|
|
189
|
+
}>>>;
|
|
190
|
+
consume<T extends InferMeteredFeatures<NonNullable<TProducts[number]["includes"]>[number]>>(data: ConsumeOptions<T>): Promise<ConsumeResult<T>>;
|
|
191
|
+
};
|
|
192
|
+
customers: {
|
|
193
|
+
get(args?: GetCustomer): Promise<GetCustomerResult>;
|
|
194
|
+
upsert(args: UpsertCustomer): Promise<boolean>;
|
|
195
|
+
list(args: ListCustomers): Promise<ListCustomersResult>;
|
|
196
|
+
delete(args: DeleteCustomer): Promise<boolean>;
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
handler(args?: {
|
|
200
|
+
basePath?: string;
|
|
201
|
+
}): (request: Request) => Promise<Response>;
|
|
202
|
+
};
|
|
203
|
+
//#endregion
|
|
204
|
+
export { BooleanFeatureOpts, CheckOptions, CheckResult, ConsumeOptions, ConsumeResult, CreateNomKit, DeleteCustomer, Feature, FeatureOpts, GetCustomer, GetCustomerResult, InferBooleanFeatures, InferMeteredFeatures, Interval, ListCustomers, ListCustomersResult, MeteredFeatureOpts, MeteredOpts, NomKitOptions, NombaProvider, NomkitContext, Plan, Retries, Retry, SubscribeOptions, SubscribeResult, UpsertCustomer, createNomkit, feature, plan };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { NombaAPI } from "./core/nomba_api/index.js";
|
|
2
|
+
import { createRouterMiddleware } from "./lib/utils.js";
|
|
3
|
+
import { routes } from "./endpoints/routes.js";
|
|
4
|
+
import { createQueueClient } from "./queue/client/index.js";
|
|
5
|
+
import { createRouter } from "better-call";
|
|
6
|
+
import ms from "ms";
|
|
7
|
+
//#region index.ts
|
|
8
|
+
const DAYLY_INTERVAL = Symbol("24 hours");
|
|
9
|
+
const MONTHLY_INTERVAL = Symbol("28 days");
|
|
10
|
+
const ANNUALLY_INTERVAL = Symbol("1 year");
|
|
11
|
+
const Interval = {
|
|
12
|
+
/** Defaults to `"24 hours"` */
|
|
13
|
+
DAYLY: DAYLY_INTERVAL,
|
|
14
|
+
/** Approx to `"28 days"` every cycle */
|
|
15
|
+
MONTHLY: MONTHLY_INTERVAL,
|
|
16
|
+
/** Period of `"1 year"` every cycle */
|
|
17
|
+
ANNUALLY: ANNUALLY_INTERVAL,
|
|
18
|
+
custom(every) {
|
|
19
|
+
return {
|
|
20
|
+
interval: "custom",
|
|
21
|
+
every
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const Retry = { after(after) {
|
|
26
|
+
return {
|
|
27
|
+
interval: "custom",
|
|
28
|
+
after
|
|
29
|
+
};
|
|
30
|
+
} };
|
|
31
|
+
function feature(args) {
|
|
32
|
+
if (args.type === "boolean") return (() => args);
|
|
33
|
+
return ((input) => ({
|
|
34
|
+
...args,
|
|
35
|
+
...input
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
function plan(options) {
|
|
39
|
+
enforceUniqueFeatureIds(options.includes);
|
|
40
|
+
const planPrice = enforceAmountInNaira(options.price);
|
|
41
|
+
let finalInterval = "";
|
|
42
|
+
const interval = planPrice?.interval;
|
|
43
|
+
if (interval === DAYLY_INTERVAL) finalInterval = DAYLY_INTERVAL.description;
|
|
44
|
+
if (interval === MONTHLY_INTERVAL) finalInterval = MONTHLY_INTERVAL.description;
|
|
45
|
+
if (interval === ANNUALLY_INTERVAL) finalInterval = ANNUALLY_INTERVAL.description;
|
|
46
|
+
if (typeof interval === "object" && interval?.interval === "custom") {
|
|
47
|
+
const milliseconds = ms(interval.every);
|
|
48
|
+
if (typeof milliseconds !== "number" || Number.isNaN(milliseconds)) throw new Error("You have an invalid interval");
|
|
49
|
+
if (milliseconds < 0) throw new Error("timestamp cannot include negative intervals");
|
|
50
|
+
finalInterval = interval.every;
|
|
51
|
+
}
|
|
52
|
+
const results = {
|
|
53
|
+
id: options.id,
|
|
54
|
+
label: options.label,
|
|
55
|
+
includes: options.includes
|
|
56
|
+
};
|
|
57
|
+
if (planPrice !== null) results.price = {
|
|
58
|
+
amount: Number(planPrice.amount),
|
|
59
|
+
interval: finalInterval
|
|
60
|
+
};
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
function createNomkit(args) {
|
|
64
|
+
if (!args) throw new Error("Missing required createNomkit arguments");
|
|
65
|
+
enforceUniqueProductIds(args.products);
|
|
66
|
+
const isSandbox = (process.env.IS_CREATE_NOMKIT_SANDBOX ?? "")?.length > 0;
|
|
67
|
+
const nomba = new NombaAPI({
|
|
68
|
+
accountId: args.paymentProviders.nomba.accountId,
|
|
69
|
+
clientId: args.paymentProviders.nomba.clientId,
|
|
70
|
+
clientSecret: args.paymentProviders.nomba.clientSecret,
|
|
71
|
+
webhookSecret: args.paymentProviders.nomba.webhookSecret,
|
|
72
|
+
environment: isSandbox ? "sandbox" : "production"
|
|
73
|
+
});
|
|
74
|
+
nomba.init();
|
|
75
|
+
const queueClient = createQueueClient({ baseURL: args.queueConfig.baseURL });
|
|
76
|
+
const routerContext = {
|
|
77
|
+
database: args.database,
|
|
78
|
+
nomba,
|
|
79
|
+
queueClient,
|
|
80
|
+
on: args.on
|
|
81
|
+
};
|
|
82
|
+
const router = createRouter(routes, {
|
|
83
|
+
routerContext,
|
|
84
|
+
routerMiddleware: [...createRouterMiddleware(async () => {})]
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
options: args,
|
|
88
|
+
api: {
|
|
89
|
+
async subscribe(data) {
|
|
90
|
+
return router.endpoints.subscribe({
|
|
91
|
+
body: data,
|
|
92
|
+
context: routerContext
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* Entitlements represent what a customer can currently do based on
|
|
97
|
+
* their active plan.
|
|
98
|
+
*/
|
|
99
|
+
entitlements: {
|
|
100
|
+
async check(data) {
|
|
101
|
+
return router.endpoints.checkEntitlements({
|
|
102
|
+
body: data,
|
|
103
|
+
context: routerContext
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
async consume(data) {
|
|
107
|
+
return router.endpoints.consumeEntitlements({
|
|
108
|
+
body: data,
|
|
109
|
+
context: routerContext
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
customers: {
|
|
114
|
+
async get(args) {
|
|
115
|
+
return router.endpoints.getCustomer({
|
|
116
|
+
body: args ?? {},
|
|
117
|
+
context: routerContext
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
async upsert(args) {
|
|
121
|
+
return router.endpoints.upsertCustomer({
|
|
122
|
+
body: args,
|
|
123
|
+
context: routerContext
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
async list(args) {
|
|
127
|
+
return router.endpoints.listCustomers({
|
|
128
|
+
body: args,
|
|
129
|
+
context: routerContext
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
async delete(args) {
|
|
133
|
+
return router.endpoints.deleteCustomer({
|
|
134
|
+
body: args,
|
|
135
|
+
context: routerContext
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
handler(args) {
|
|
141
|
+
const { basePath = "/nomkit" } = args ?? {};
|
|
142
|
+
return async (request) => {
|
|
143
|
+
const url = new URL(request.url);
|
|
144
|
+
if (url.pathname.startsWith(basePath)) url.pathname = url.pathname.slice(basePath.length) || "/";
|
|
145
|
+
return router.handler(new Request(url, request));
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function enforceUniqueProductIds(products = []) {
|
|
151
|
+
products.reduce((acc, curr) => {
|
|
152
|
+
if (acc.includes(curr.id)) throw new Error(`Product with id: "${curr.id}" already exists.`);
|
|
153
|
+
return [...acc, curr.id];
|
|
154
|
+
}, []);
|
|
155
|
+
}
|
|
156
|
+
function enforceUniqueFeatureIds(features = []) {
|
|
157
|
+
features.reduce((acc, curr) => {
|
|
158
|
+
if (acc.includes(curr.id)) throw new Error(`Feature with id: "${curr.id}" already exists.`);
|
|
159
|
+
return [...acc, curr.id];
|
|
160
|
+
}, []);
|
|
161
|
+
}
|
|
162
|
+
function enforceAmountInNaira(plan) {
|
|
163
|
+
if (plan == null) return null;
|
|
164
|
+
const { amount } = plan;
|
|
165
|
+
if (typeof amount !== "number") throw new TypeError("Plan amount must be a number expressed in naira.");
|
|
166
|
+
if (!Number.isFinite(amount)) throw new TypeError("Plan amount must be a finite number.");
|
|
167
|
+
if (amount <= 0) throw new RangeError("Plan amount must be greater than zero.");
|
|
168
|
+
if (amount < 100) throw new RangeError("Plan amount must be at least ₦100.00.");
|
|
169
|
+
return {
|
|
170
|
+
amount: amount.toFixed(2),
|
|
171
|
+
interval: plan.interval
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { Interval, Retry, createNomkit, feature, plan };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Endpoint, EndpointContext } from "better-call";
|
|
2
|
+
import { z as z$1 } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region lib/utils.d.ts
|
|
5
|
+
type DefineMethod<TSchema extends z$1.ZodType, TPath extends string, TRequireHeaders extends boolean = boolean, TRequireRequest extends boolean = boolean> = {
|
|
6
|
+
input: TSchema;
|
|
7
|
+
route: {
|
|
8
|
+
path: TPath;
|
|
9
|
+
requireHeaders?: TRequireRequest;
|
|
10
|
+
requireRequest?: TRequireHeaders;
|
|
11
|
+
clientOnly?: boolean;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
type InferEndpoint<T> = T extends Endpoint<any, any, infer Request, any, any, infer Response, infer Metadata, any> ? {
|
|
15
|
+
request: Request;
|
|
16
|
+
response: Response;
|
|
17
|
+
metadata: Metadata;
|
|
18
|
+
} : never;
|
|
19
|
+
type InferRequired<R> = R extends boolean ? R : false;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { DefineMethod, InferEndpoint, InferRequired };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getDrizzle } from "../core/pg_db/index.js";
|
|
2
|
+
import { createEndpoint, createMiddleware } from "better-call";
|
|
3
|
+
import { customAlphabet } from "nanoid";
|
|
4
|
+
//#region lib/utils.ts
|
|
5
|
+
const DEFAULT_PREFIX = "nomkit_order_";
|
|
6
|
+
const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-", 18);
|
|
7
|
+
const getOrderReference = (prefix = DEFAULT_PREFIX) => {
|
|
8
|
+
const id = generateId();
|
|
9
|
+
return prefix ? `${prefix}${id}` : id;
|
|
10
|
+
};
|
|
11
|
+
createMiddleware(async () => void 0);
|
|
12
|
+
function defineNomkitMethod(args, handler) {
|
|
13
|
+
return createEndpoint(args.route.path, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
body: args.input,
|
|
16
|
+
...args.route.clientOnly === true && { metadata: { scope: "http" } }
|
|
17
|
+
}, async (ctx) => {
|
|
18
|
+
const { context = {}, body, ...baseCtx } = ctx;
|
|
19
|
+
const { database, ...nomkitCtx } = context;
|
|
20
|
+
const db = getDrizzle(database);
|
|
21
|
+
return handler({
|
|
22
|
+
...baseCtx,
|
|
23
|
+
nomkit: {
|
|
24
|
+
...nomkitCtx,
|
|
25
|
+
database: db
|
|
26
|
+
},
|
|
27
|
+
input: body
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const createRouterMiddleware = (cb) => {
|
|
32
|
+
const path = "/**";
|
|
33
|
+
return [{
|
|
34
|
+
path,
|
|
35
|
+
middleware: createEndpoint(path, { method: "*" }, async (ctx) => {
|
|
36
|
+
await cb?.();
|
|
37
|
+
})
|
|
38
|
+
}];
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { createRouterMiddleware, defineNomkitMethod, getOrderReference };
|