@arnaudjnn/billing-tools 0.14.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +243 -0
  3. package/dist/adapters/workos-org.d.ts +80 -0
  4. package/dist/adapters/workos-org.d.ts.map +1 -0
  5. package/dist/adapters/workos-org.js +176 -0
  6. package/dist/adapters/workos-org.js.map +1 -0
  7. package/dist/agent-auth/claim-store.d.ts +37 -0
  8. package/dist/agent-auth/claim-store.d.ts.map +1 -0
  9. package/dist/agent-auth/claim-store.js +64 -0
  10. package/dist/agent-auth/claim-store.js.map +1 -0
  11. package/dist/agent-auth/index.d.ts +65 -0
  12. package/dist/agent-auth/index.d.ts.map +1 -0
  13. package/dist/agent-auth/index.js +365 -0
  14. package/dist/agent-auth/index.js.map +1 -0
  15. package/dist/auth.d.ts +16 -0
  16. package/dist/auth.d.ts.map +1 -0
  17. package/dist/auth.js +80 -0
  18. package/dist/auth.js.map +1 -0
  19. package/dist/billing.d.ts +59 -0
  20. package/dist/billing.d.ts.map +1 -0
  21. package/dist/billing.js +258 -0
  22. package/dist/billing.js.map +1 -0
  23. package/dist/cli/client.d.ts +10 -0
  24. package/dist/cli/client.d.ts.map +1 -0
  25. package/dist/cli/client.js +22 -0
  26. package/dist/cli/client.js.map +1 -0
  27. package/dist/cli/commands.d.ts +4 -0
  28. package/dist/cli/commands.d.ts.map +1 -0
  29. package/dist/cli/commands.js +94 -0
  30. package/dist/cli/commands.js.map +1 -0
  31. package/dist/cli/config.d.ts +16 -0
  32. package/dist/cli/config.d.ts.map +1 -0
  33. package/dist/cli/config.js +40 -0
  34. package/dist/cli/config.js.map +1 -0
  35. package/dist/create-billing.d.ts +78 -0
  36. package/dist/create-billing.d.ts.map +1 -0
  37. package/dist/create-billing.js +59 -0
  38. package/dist/create-billing.js.map +1 -0
  39. package/dist/dispatch.d.ts +9 -0
  40. package/dist/dispatch.d.ts.map +1 -0
  41. package/dist/dispatch.js +73 -0
  42. package/dist/dispatch.js.map +1 -0
  43. package/dist/events.d.ts +26 -0
  44. package/dist/events.d.ts.map +1 -0
  45. package/dist/events.js +72 -0
  46. package/dist/events.js.map +1 -0
  47. package/dist/index.d.ts +24 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +40 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/invitations.d.ts +56 -0
  52. package/dist/invitations.d.ts.map +1 -0
  53. package/dist/invitations.js +117 -0
  54. package/dist/invitations.js.map +1 -0
  55. package/dist/machine-payment/index.d.ts +57 -0
  56. package/dist/machine-payment/index.d.ts.map +1 -0
  57. package/dist/machine-payment/index.js +117 -0
  58. package/dist/machine-payment/index.js.map +1 -0
  59. package/dist/magic-auth.d.ts +4 -0
  60. package/dist/magic-auth.d.ts.map +1 -0
  61. package/dist/magic-auth.js +30 -0
  62. package/dist/magic-auth.js.map +1 -0
  63. package/dist/mirror.d.ts +35 -0
  64. package/dist/mirror.d.ts.map +1 -0
  65. package/dist/mirror.js +63 -0
  66. package/dist/mirror.js.map +1 -0
  67. package/dist/plans.d.ts +38 -0
  68. package/dist/plans.d.ts.map +1 -0
  69. package/dist/plans.js +132 -0
  70. package/dist/plans.js.map +1 -0
  71. package/dist/routes/mcp.d.ts +19 -0
  72. package/dist/routes/mcp.d.ts.map +1 -0
  73. package/dist/routes/mcp.js +39 -0
  74. package/dist/routes/mcp.js.map +1 -0
  75. package/dist/routes/rest.d.ts +20 -0
  76. package/dist/routes/rest.d.ts.map +1 -0
  77. package/dist/routes/rest.js +66 -0
  78. package/dist/routes/rest.js.map +1 -0
  79. package/dist/routes/webhook.d.ts +8 -0
  80. package/dist/routes/webhook.d.ts.map +1 -0
  81. package/dist/routes/webhook.js +39 -0
  82. package/dist/routes/webhook.js.map +1 -0
  83. package/dist/sync.d.ts +58 -0
  84. package/dist/sync.d.ts.map +1 -0
  85. package/dist/sync.js +202 -0
  86. package/dist/sync.js.map +1 -0
  87. package/dist/tools/billing.d.ts +4 -0
  88. package/dist/tools/billing.d.ts.map +1 -0
  89. package/dist/tools/billing.js +125 -0
  90. package/dist/tools/billing.js.map +1 -0
  91. package/dist/tools/keys.d.ts +4 -0
  92. package/dist/tools/keys.d.ts.map +1 -0
  93. package/dist/tools/keys.js +107 -0
  94. package/dist/tools/keys.js.map +1 -0
  95. package/dist/tools/register.d.ts +20 -0
  96. package/dist/tools/register.d.ts.map +1 -0
  97. package/dist/tools/register.js +87 -0
  98. package/dist/tools/register.js.map +1 -0
  99. package/dist/types.d.ts +90 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +28 -0
  102. package/dist/types.js.map +1 -0
  103. package/dist/util/clearout.d.ts +5 -0
  104. package/dist/util/clearout.d.ts.map +1 -0
  105. package/dist/util/clearout.js +18 -0
  106. package/dist/util/clearout.js.map +1 -0
  107. package/dist/workos.d.ts +6 -0
  108. package/dist/workos.d.ts.map +1 -0
  109. package/dist/workos.js +24 -0
  110. package/dist/workos.js.map +1 -0
  111. package/package.json +81 -0
@@ -0,0 +1,117 @@
1
+ import { randomBytes } from "node:crypto";
2
+ const PROBLEM_TYPE = "https://paymentauth.org/problems/payment-required";
3
+ function challengeHeader(c) {
4
+ const parts = [
5
+ `id="${c.id}"`,
6
+ `method="${c.method}"`,
7
+ `intent="${c.intent}"`,
8
+ `amount="${c.amount}"`,
9
+ `currency="${c.currency}"`,
10
+ ...(c.payTo ? [`payTo="${c.payTo}"`] : []),
11
+ ];
12
+ return `Payment ${parts.join(", ")}`;
13
+ }
14
+ /** Read the payment credential the client presents on retry. MPP clients send
15
+ * it in an `Authorization: Payment <credential>` header (we also accept the
16
+ * common `X-Payment` header). */
17
+ function readCredential(request) {
18
+ const auth = request.headers.get("authorization");
19
+ if (auth?.startsWith("Payment "))
20
+ return auth.slice(8).trim();
21
+ return request.headers.get("x-payment");
22
+ }
23
+ /** Build the MPP payment gate. Returns `requirePayment(request)` → either a 402
24
+ * `Response` (payment needed / rejected / not-settled) or `{ paid: true,
25
+ * challenge, receipt }` when settlement succeeds — the caller then serves the
26
+ * paid resource. */
27
+ export function createMachinePaymentHandler(opts) {
28
+ const methods = opts.methods ?? ["stripe"];
29
+ const currency = opts.currency ?? "usd";
30
+ async function buildChallenges(request) {
31
+ const amount = typeof opts.amount === "function" ? await opts.amount(request) : opts.amount;
32
+ return methods.map((method) => ({
33
+ id: `chal_${randomBytes(12).toString("hex")}`,
34
+ method,
35
+ intent: "charge",
36
+ amount,
37
+ currency,
38
+ payTo: method === "stripe" ? opts.networkId : opts.payToAddress,
39
+ }));
40
+ }
41
+ function paymentRequired(challenges, detail) {
42
+ const headers = new Headers({ "Content-Type": "application/problem+json", "Cache-Control": "no-store" });
43
+ for (const c of challenges)
44
+ headers.append("WWW-Authenticate", challengeHeader(c));
45
+ return new Response(JSON.stringify({
46
+ type: PROBLEM_TYPE,
47
+ title: "Payment Required",
48
+ status: 402,
49
+ detail,
50
+ challengeId: challenges[0]?.id,
51
+ accepts: challenges.map((c) => ({ method: c.method, amount: c.amount, currency: c.currency, payTo: c.payTo })),
52
+ }), { status: 402, headers });
53
+ }
54
+ async function requirePayment(request) {
55
+ const credential = readCredential(request);
56
+ const challenges = await buildChallenges(request);
57
+ if (!credential)
58
+ return paymentRequired(challenges, "Payment is required.");
59
+ if (!opts.settle) {
60
+ return paymentRequired(challenges, "Payment settlement is not enabled on this account yet.");
61
+ }
62
+ // Settle against the first challenge (single-method retries are typical).
63
+ const challenge = challenges[0];
64
+ try {
65
+ const result = await opts.settle(credential, challenge);
66
+ if (!result)
67
+ return paymentRequired(challenges, "Payment was declined.");
68
+ await opts.onPaid?.(challenge, result.receipt, request);
69
+ return { paid: true, challenge, receipt: result.receipt };
70
+ }
71
+ catch (e) {
72
+ return paymentRequired(challenges, `Payment failed: ${e instanceof Error ? e.message : String(e)}`);
73
+ }
74
+ }
75
+ return { requirePayment, buildChallenges };
76
+ }
77
+ /** `/payment.md` — the agent-facing "how to pay per request" narrative; the
78
+ * payment equivalent of auth.md. */
79
+ export function createPaymentMd(opts) {
80
+ return function paymentMd(request) {
81
+ const b = typeof opts.baseUrl === "function"
82
+ ? opts.baseUrl(request)
83
+ : (opts.baseUrl ??
84
+ (() => {
85
+ const h = request.headers;
86
+ const host = h.get("x-forwarded-host") ?? h.get("host") ?? new URL(request.url).host;
87
+ const proto = h.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https");
88
+ return `${proto}://${host}`;
89
+ })());
90
+ const methods = (opts.methods ?? ["stripe"]).join(", ");
91
+ const path = opts.resourcePath ?? "/mcp";
92
+ const body = `# ${opts.productName} — payment.md
93
+
94
+ This service accepts per-request machine payments via [MPP](https://mpp.dev) (the Machine Payments Protocol). As an agent you can pay for a resource on demand — no account or API key required.
95
+
96
+ ## Flow
97
+
98
+ 1. Request the paid resource:
99
+ \`\`\`http
100
+ POST ${b}${path}
101
+ \`\`\`
102
+ 2. If payment is required you get **HTTP 402** with one \`WWW-Authenticate: Payment …\` header per accepted method and an \`application/problem+json\` body describing the amount, currency, and \`payTo\`.
103
+ 3. Authorize the payment (methods: ${methods}) and retry the request with the credential:
104
+ \`\`\`http
105
+ POST ${b}${path}
106
+ Authorization: Payment <credential>
107
+ \`\`\`
108
+ 4. On success you get the resource plus a \`Payment-Receipt\` header.
109
+
110
+ Accepted currency: ${opts.currency ?? "usd"}. See https://docs.stripe.com/payments/machine for client tooling.
111
+ `;
112
+ return new Response(body, {
113
+ headers: { "content-type": "text/markdown; charset=utf-8", "cache-control": "public, max-age=3600" },
114
+ });
115
+ };
116
+ }
117
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/machine-payment/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { BillingUser } from "./types.js";
2
+ export declare function sendMagicAuth(email: string): Promise<void>;
3
+ export declare function verifyMagicAuth(email: string, code: string): Promise<BillingUser>;
4
+ //# sourceMappingURL=magic-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic-auth.d.ts","sourceRoot":"","sources":["../src/magic-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAQ9C,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhE;AAGD,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAcvF"}
@@ -0,0 +1,30 @@
1
+ import { getWorkOS as workos } from "./workos.js";
2
+ // WorkOS magic-auth — the common auth substrate for billing-tools. The host
3
+ // app's storage (WorkOS orgs vs Postgres) only matters *after* the user is
4
+ // verified; that post-auth step is the adapter's ensureOrgForUser.
5
+ // Step 1: ensure the user exists in WorkOS and email them a 6-digit code.
6
+ export async function sendMagicAuth(email) {
7
+ const wos = workos();
8
+ const users = await wos.userManagement.listUsers({ email });
9
+ if (users.data.length === 0) {
10
+ await wos.userManagement.createUser({ email, emailVerified: false });
11
+ }
12
+ await wos.userManagement.createMagicAuth({ email });
13
+ }
14
+ // Step 2: verify the code, returning the WorkOS user.
15
+ export async function verifyMagicAuth(email, code) {
16
+ const res = await workos().userManagement.authenticateWithMagicAuth({
17
+ code,
18
+ email,
19
+ clientId: process.env.WORKOS_CLIENT_ID,
20
+ });
21
+ const u = res.user;
22
+ return {
23
+ id: u.id,
24
+ email: u.email,
25
+ firstName: u.firstName,
26
+ lastName: u.lastName,
27
+ profilePictureUrl: u.profilePictureUrl,
28
+ };
29
+ }
30
+ //# sourceMappingURL=magic-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic-auth.js","sourceRoot":"","sources":["../src/magic-auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC;AAElD,4EAA4E;AAC5E,2EAA2E;AAC3E,mEAAmE;AAEnE,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAa;IAC/C,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5D,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,GAAG,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,GAAG,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAa,EAAE,IAAY;IAC/D,MAAM,GAAG,GAAG,MAAM,MAAM,EAAE,CAAC,cAAc,CAAC,yBAAyB,CAAC;QAClE,IAAI;QACJ,KAAK;QACL,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAiB;KACxC,CAAC,CAAC;IACH,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;IACnB,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;KACvC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,35 @@
1
+ export type MirrorQueryResult = {
2
+ rows: Array<Record<string, unknown>>;
3
+ rowCount: number | null;
4
+ };
5
+ export type MirrorQuery = (sql: string, params: unknown[]) => Promise<MirrorQueryResult>;
6
+ export interface MirrorOptions {
7
+ /** Table name (identifier — validated). */
8
+ table: string;
9
+ /** WorkOS id column, e.g. "workos_org_id" or "workos_user_id". */
10
+ idColumn: string;
11
+ /** JSON metadata column. Default "workos_metadata". */
12
+ metadataColumn?: string;
13
+ /** Optional map of WorkOS-resource field → DB column, synced by syncResource
14
+ * (e.g. { name: "name" } to keep a workspaces.name column in sync). */
15
+ columns?: Record<string, string>;
16
+ /** Query executor returning { rows, rowCount }. Keeps this package pg-free. */
17
+ query: MirrorQuery;
18
+ }
19
+ export interface Mirror {
20
+ /** Insert the row from id + metadata if absent (no-op if it exists). */
21
+ ensure(workosId: string, metadata?: Record<string, unknown>): Promise<void>;
22
+ /** Full row, or null. */
23
+ get(workosId: string): Promise<Record<string, unknown> | null>;
24
+ /** Just the metadata blob, or null if the row is absent. */
25
+ getMetadata(workosId: string): Promise<Record<string, unknown> | null>;
26
+ /** Overwrite the metadata blob on an existing row. */
27
+ setMetadata(workosId: string, metadata: Record<string, unknown>): Promise<void>;
28
+ /** Sync a WorkOS resource onto the existing row: the mapped `columns` fields
29
+ * + the metadata blob. Update-only (rows are created by the app). */
30
+ syncResource(workosId: string, resource: Record<string, unknown>): Promise<void>;
31
+ /** Delete the row. */
32
+ remove(workosId: string): Promise<void>;
33
+ }
34
+ export declare function createMirror(opts: MirrorOptions): Mirror;
35
+ //# sourceMappingURL=mirror.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mirror.d.ts","sourceRoot":"","sources":["../src/mirror.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,iBAAiB,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAClG,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEzF,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,+EAA+E;IAC/E,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,MAAM,WAAW,MAAM;IACrB,wEAAwE;IACxE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,yBAAyB;IACzB,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC/D,4DAA4D;IAC5D,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACvE,sDAAsD;IACtD,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChF;0EACsE;IACtE,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,sBAAsB;IACtB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAMD,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAiDxD"}
package/dist/mirror.js ADDED
@@ -0,0 +1,63 @@
1
+ // Generic, table-agnostic DB mirror for WorkOS entities. WorkOS stays the
2
+ // source of truth; an app that keeps a database shadows each WorkOS org/user in
3
+ // a local row so it can join/query without a round-trip. This helper owns just
4
+ // two columns — the WorkOS id and a `workos_metadata` JSON blob — and leaves
5
+ // every other column on the table to the app.
6
+ //
7
+ // It's DB-free: you pass a `query` executor (e.g. a thin wrapper over `pg`), so
8
+ // billing-tools never depends on a database driver. Two conventional shapes:
9
+ // - org mirror: { workos_org_id, workos_metadata }
10
+ // - user mirror: { workos_user_id, workos_metadata }
11
+ //
12
+ // `ensure` inserts a row from the id + metadata alone, so it only fits tables
13
+ // whose other columns are nullable/defaulted (e.g. a users table). For a table
14
+ // with required app columns (e.g. a workspaces table the app inserts itself),
15
+ // use `setMetadata` to update the blob on the already-created row.
16
+ // Identifiers are developer config (not user input), but validate anyway since
17
+ // they're interpolated into SQL.
18
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
19
+ export function createMirror(opts) {
20
+ const { table, idColumn, query } = opts;
21
+ const meta = opts.metadataColumn ?? "workos_metadata";
22
+ const columns = opts.columns ?? {};
23
+ for (const ident of [table, idColumn, meta, ...Object.values(columns)]) {
24
+ if (!IDENTIFIER.test(ident)) {
25
+ throw new Error(`createMirror: invalid SQL identifier "${ident}"`);
26
+ }
27
+ }
28
+ return {
29
+ async ensure(workosId, metadata = {}) {
30
+ await query(`INSERT INTO ${table} (${idColumn}, ${meta}) VALUES ($1, $2)
31
+ ON CONFLICT (${idColumn}) DO NOTHING`, [workosId, JSON.stringify(metadata)]);
32
+ },
33
+ async get(workosId) {
34
+ const r = await query(`SELECT * FROM ${table} WHERE ${idColumn} = $1`, [workosId]);
35
+ return r.rows[0] ?? null;
36
+ },
37
+ async getMetadata(workosId) {
38
+ const r = await query(`SELECT ${meta} AS metadata FROM ${table} WHERE ${idColumn} = $1`, [workosId]);
39
+ return r.rows[0]?.metadata ?? null;
40
+ },
41
+ async setMetadata(workosId, metadata) {
42
+ await query(`UPDATE ${table} SET ${meta} = $2 WHERE ${idColumn} = $1`, [
43
+ workosId,
44
+ JSON.stringify(metadata),
45
+ ]);
46
+ },
47
+ async syncResource(workosId, resource) {
48
+ const params = [workosId];
49
+ const sets = [];
50
+ for (const [field, column] of Object.entries(columns)) {
51
+ params.push(resource[field] ?? null);
52
+ sets.push(`${column} = $${params.length}`);
53
+ }
54
+ params.push(JSON.stringify(resource.metadata ?? {}));
55
+ sets.push(`${meta} = $${params.length}`);
56
+ await query(`UPDATE ${table} SET ${sets.join(", ")} WHERE ${idColumn} = $1`, params);
57
+ },
58
+ async remove(workosId) {
59
+ await query(`DELETE FROM ${table} WHERE ${idColumn} = $1`, [workosId]);
60
+ },
61
+ };
62
+ }
63
+ //# sourceMappingURL=mirror.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mirror.js","sourceRoot":"","sources":["../src/mirror.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ export interface PlanDef {
2
+ /** Max members per workspace. null = unlimited. */
3
+ seats: number | null;
4
+ /** Included tokens granted per seat, per billing cycle. */
5
+ tokensPerSeat: number;
6
+ /** Recurring price in the smallest currency unit (cents). 0 = free (no Stripe price). */
7
+ price: {
8
+ monthly: number;
9
+ yearly: number;
10
+ };
11
+ }
12
+ export type PlansConfig = Record<string, PlanDef>;
13
+ export type BillingInterval = "monthly" | "yearly";
14
+ export declare const lookupKeyFor: (plan: string, interval: BillingInterval) => string;
15
+ export interface EnsuredPrice {
16
+ plan: string;
17
+ interval: BillingInterval;
18
+ priceId: string;
19
+ productId: string;
20
+ amount: number;
21
+ lookupKey: string;
22
+ }
23
+ /** Idempotently create/reconcile Stripe products + prices for the paid plans.
24
+ * Returns the resolved price for every paid plan × interval. Free plans (both
25
+ * prices 0) create no Stripe objects. Safe to call on every boot / first use. */
26
+ export declare function ensurePlans(plans: PlansConfig, opts?: {
27
+ currency?: string;
28
+ }): Promise<EnsuredPrice[]>;
29
+ /** Resolve the current Stripe price id for a plan + interval (via lookup_key).
30
+ * Returns null for a free/absent price. */
31
+ export declare function planPriceId(plan: string, interval: BillingInterval): Promise<string | null>;
32
+ /** Reverse-map a Stripe price id → plan key (via the price's metadata). */
33
+ export declare function planForPriceId(priceId: string): Promise<string | null>;
34
+ /** Seat limit for a plan (null = unlimited, undefined plan = null). */
35
+ export declare function seatLimit(plans: PlansConfig, plan: string): number | null;
36
+ /** Included tokens for `seatCount` members on a plan (per cycle). */
37
+ export declare function includedTokens(plans: PlansConfig, plan: string, seatCount: number): number;
38
+ //# sourceMappingURL=plans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../src/plans.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,OAAO;IACtB,mDAAmD;IACnD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAClD,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,QAAQ,CAAC;AASnD,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,UAAU,eAAe,KAAG,MAChD,CAAC;AAExB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAsBD;;kFAEkF;AAClF,wBAAsB,WAAW,CAC/B,KAAK,EAAE,WAAW,EAClB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,YAAY,EAAE,CAAC,CAsDzB;AAwBD;4CAC4C;AAC5C,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,eAAe,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOxB;AAED,2EAA2E;AAC3E,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO5E;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEzE;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAC5B,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,MAAM,CAIR"}
package/dist/plans.js ADDED
@@ -0,0 +1,132 @@
1
+ import { getStripe } from "./billing.js";
2
+ const INTERVALS = ["monthly", "yearly"];
3
+ const STRIPE_INTERVAL = {
4
+ monthly: "month",
5
+ yearly: "year",
6
+ };
7
+ const MANAGED_BY = "billing-tools";
8
+ export const lookupKeyFor = (plan, interval) => `${plan}_${interval}`;
9
+ const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
10
+ /** The single managed Stripe product for a plan (reuse across price versions),
11
+ * discovered via any existing managed price's product. */
12
+ async function findPlanProduct(stripe, plan) {
13
+ for (const interval of INTERVALS) {
14
+ const found = await stripe.prices.list({
15
+ lookup_keys: [lookupKeyFor(plan, interval)],
16
+ limit: 1,
17
+ expand: ["data.product"],
18
+ });
19
+ const p = found.data[0];
20
+ if (p)
21
+ return typeof p.product === "string" ? p.product : p.product.id;
22
+ }
23
+ return null;
24
+ }
25
+ /** Idempotently create/reconcile Stripe products + prices for the paid plans.
26
+ * Returns the resolved price for every paid plan × interval. Free plans (both
27
+ * prices 0) create no Stripe objects. Safe to call on every boot / first use. */
28
+ export async function ensurePlans(plans, opts = {}) {
29
+ const stripe = getStripe();
30
+ const currency = (opts.currency ?? "usd").toLowerCase();
31
+ const result = [];
32
+ const wanted = new Set();
33
+ for (const [plan, def] of Object.entries(plans)) {
34
+ let productId = await findPlanProduct(stripe, plan);
35
+ for (const interval of INTERVALS) {
36
+ const amount = def.price[interval];
37
+ if (!amount || amount <= 0)
38
+ continue; // free interval — no Stripe price
39
+ const lookupKey = lookupKeyFor(plan, interval);
40
+ wanted.add(lookupKey);
41
+ const existing = (await stripe.prices.list({ lookup_keys: [lookupKey], active: true, limit: 1 })).data[0];
42
+ const matches = existing &&
43
+ existing.unit_amount === amount &&
44
+ existing.currency === currency &&
45
+ existing.recurring?.interval === STRIPE_INTERVAL[interval];
46
+ if (existing && matches) {
47
+ productId =
48
+ typeof existing.product === "string" ? existing.product : existing.product.id;
49
+ result.push({ plan, interval, priceId: existing.id, productId, amount, lookupKey });
50
+ continue;
51
+ }
52
+ if (!productId) {
53
+ const product = await stripe.products.create({
54
+ name: cap(plan),
55
+ metadata: { managedBy: MANAGED_BY, plan },
56
+ });
57
+ productId = product.id;
58
+ }
59
+ // Create the new price and move the lookup_key onto it (frees it from the
60
+ // old price), then archive the old price. Subscribers on it are untouched.
61
+ const created = await stripe.prices.create({
62
+ product: productId,
63
+ currency,
64
+ unit_amount: amount,
65
+ recurring: { interval: STRIPE_INTERVAL[interval] },
66
+ lookup_key: lookupKey,
67
+ transfer_lookup_key: true,
68
+ metadata: { managedBy: MANAGED_BY, plan, interval },
69
+ });
70
+ if (existing)
71
+ await stripe.prices.update(existing.id, { active: false });
72
+ result.push({ plan, interval, priceId: created.id, productId, amount, lookupKey });
73
+ }
74
+ }
75
+ await archiveOrphans(stripe, wanted);
76
+ return result;
77
+ }
78
+ /** Archive managed prices whose lookup_key is no longer configured (a plan or
79
+ * interval you removed), and deactivate now-empty managed products. */
80
+ async function archiveOrphans(stripe, wanted) {
81
+ const managedProducts = new Set();
82
+ for await (const price of stripe.prices.list({ active: true, limit: 100 })) {
83
+ if (price.metadata?.managedBy !== MANAGED_BY)
84
+ continue;
85
+ const productId = typeof price.product === "string" ? price.product : price.product.id;
86
+ if (price.lookup_key && wanted.has(price.lookup_key)) {
87
+ managedProducts.add(productId);
88
+ continue;
89
+ }
90
+ await stripe.prices.update(price.id, { active: false });
91
+ }
92
+ // A managed product with no remaining wanted price → archive it.
93
+ for await (const product of stripe.products.list({ active: true, limit: 100 })) {
94
+ if (product.metadata?.managedBy !== MANAGED_BY)
95
+ continue;
96
+ if (!managedProducts.has(product.id)) {
97
+ await stripe.products.update(product.id, { active: false }).catch(() => { });
98
+ }
99
+ }
100
+ }
101
+ /** Resolve the current Stripe price id for a plan + interval (via lookup_key).
102
+ * Returns null for a free/absent price. */
103
+ export async function planPriceId(plan, interval) {
104
+ const r = await getStripe().prices.list({
105
+ lookup_keys: [lookupKeyFor(plan, interval)],
106
+ active: true,
107
+ limit: 1,
108
+ });
109
+ return r.data[0]?.id ?? null;
110
+ }
111
+ /** Reverse-map a Stripe price id → plan key (via the price's metadata). */
112
+ export async function planForPriceId(priceId) {
113
+ try {
114
+ const price = await getStripe().prices.retrieve(priceId);
115
+ return price.metadata?.plan ?? null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ /** Seat limit for a plan (null = unlimited, undefined plan = null). */
122
+ export function seatLimit(plans, plan) {
123
+ return plans[plan]?.seats ?? null;
124
+ }
125
+ /** Included tokens for `seatCount` members on a plan (per cycle). */
126
+ export function includedTokens(plans, plan, seatCount) {
127
+ const def = plans[plan];
128
+ if (!def)
129
+ return 0;
130
+ return def.tokensPerSeat * Math.max(1, seatCount);
131
+ }
132
+ //# sourceMappingURL=plans.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plans.js","sourceRoot":"","sources":["../src/plans.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { BillingAdapter } from "../types.js";
3
+ export interface McpTransportOptions {
4
+ register: (server: McpServer) => void;
5
+ adapter: BillingAdapter;
6
+ realm?: string;
7
+ /** Prefix that marks a raw API key (vs an OAuth JWT). Default "sk_". */
8
+ apiKeyPrefix?: string;
9
+ maxDuration?: number;
10
+ /** Advertise the auth.md PRM discovery doc in the 401 WWW-Authenticate header
11
+ * (`resource_metadata="…"`) so agents can bootstrap. String or per-request. */
12
+ resourceMetadata?: string | ((request: Request) => string);
13
+ }
14
+ export declare function createMcpTransport(opts: McpTransportOptions): {
15
+ GET: (request: Request) => Promise<Response>;
16
+ POST: (request: Request) => Promise<Response>;
17
+ maxDuration: number;
18
+ };
19
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/routes/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAQlD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IACtC,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;oFACgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;CAC5D;AAOD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB;mBAqB1B,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;oBAA3B,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;;EAgB5D"}
@@ -0,0 +1,39 @@
1
+ import { runWithAuth, runWithResolvedOrg } from "../auth.js";
2
+ function wwwAuth(realm, resourceMetadata) {
3
+ const base = `Bearer realm="${realm}", error="invalid_token"`;
4
+ return resourceMetadata ? `${base}, resource_metadata="${resourceMetadata}"` : base;
5
+ }
6
+ export function createMcpTransport(opts) {
7
+ const realm = opts.realm ?? "billing-tools";
8
+ const apiKeyPrefix = opts.apiKeyPrefix ?? "sk_";
9
+ let handlerPromise = null;
10
+ async function getHandler() {
11
+ if (!handlerPromise) {
12
+ handlerPromise = import("mcp-handler").then(({ createMcpHandler }) => createMcpHandler((server) => opts.register(server), {}, { basePath: "/", maxDuration: opts.maxDuration ?? 60 }));
13
+ }
14
+ return handlerPromise;
15
+ }
16
+ async function withAuthHeader(res, rm) {
17
+ if (res.status !== 401)
18
+ return res;
19
+ const headers = new Headers(res.headers);
20
+ if (!headers.has("WWW-Authenticate"))
21
+ headers.set("WWW-Authenticate", wwwAuth(realm, rm));
22
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
23
+ }
24
+ async function handler(request) {
25
+ const mcp = await getHandler();
26
+ const authHeader = request.headers.get("authorization");
27
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
28
+ const rm = typeof opts.resourceMetadata === "function" ? opts.resourceMetadata(request) : opts.resourceMetadata;
29
+ if (token && !token.startsWith(apiKeyPrefix) && opts.adapter.resolveOauthOrg) {
30
+ const orgId = await opts.adapter.resolveOauthOrg(token);
31
+ if (orgId)
32
+ return runWithResolvedOrg(authHeader, orgId, () => mcp(request));
33
+ }
34
+ const res = await runWithAuth(authHeader, () => mcp(request));
35
+ return withAuthHeader(res, rm);
36
+ }
37
+ return { GET: handler, POST: handler, maxDuration: opts.maxDuration ?? 60 };
38
+ }
39
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/routes/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface Dispatcher {
2
+ dispatchTool(name: string, args: Record<string, unknown>): Promise<unknown>;
3
+ getToolNames(): string[];
4
+ }
5
+ export declare function createToolListHandler(opts: {
6
+ dispatcher: Dispatcher;
7
+ toolCosts?: Record<string, number>;
8
+ }): (request: Request) => Promise<Response>;
9
+ export declare function createToolDispatchHandler(opts: {
10
+ dispatcher: Dispatcher;
11
+ realm?: string;
12
+ /** Advertise the auth.md PRM discovery doc in the 401 WWW-Authenticate header
13
+ * (`resource_metadata="…"`) so agents can bootstrap. String or per-request. */
14
+ resourceMetadata?: string | ((request: Request) => string);
15
+ }): (request: Request, ctx: {
16
+ params: Promise<{
17
+ tool: string;
18
+ }>;
19
+ }) => Promise<Response>;
20
+ //# sourceMappingURL=rest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/routes/rest.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,UAAU;IACzB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5E,YAAY,IAAI,MAAM,EAAE,CAAC;CAC1B;AAQD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC,IACe,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAanD;AAUD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,UAAU,EAAE,UAAU,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;oFACgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;CAC5D,IAGG,SAAS,OAAO,EAChB,KAAK;IAAE,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,KACzC,OAAO,CAAC,QAAQ,CAAC,CA+BrB"}
@@ -0,0 +1,66 @@
1
+ import { runWithAuth } from "../auth.js";
2
+ import { ToolValidationError } from "../dispatch.js";
3
+ function wwwAuth(realm, resourceMetadata) {
4
+ const base = `Bearer realm="${realm}", error="invalid_token"`;
5
+ return resourceMetadata ? `${base}, resource_metadata="${resourceMetadata}"` : base;
6
+ }
7
+ // GET /api/v0 → { tools: [{ name, cost }] }
8
+ export function createToolListHandler(opts) {
9
+ return async (request) => {
10
+ const authHeader = request.headers.get("authorization");
11
+ return runWithAuth(authHeader, async () => {
12
+ try {
13
+ const names = opts.dispatcher.getToolNames();
14
+ return Response.json({
15
+ tools: names.map((name) => ({ name, cost: opts.toolCosts?.[name] ?? 0 })),
16
+ });
17
+ }
18
+ catch (err) {
19
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
20
+ }
21
+ });
22
+ };
23
+ }
24
+ function isUnauthorizedResult(result) {
25
+ if (!result || typeof result !== "object")
26
+ return false;
27
+ const r = result;
28
+ if (!r.isError || !Array.isArray(r.content))
29
+ return false;
30
+ return r.content.some((c) => typeof c.text === "string" && /\bUnauthorized\b/i.test(c.text));
31
+ }
32
+ // POST /api/v0/[tool] → dispatch. Next passes ctx.params as a Promise.
33
+ export function createToolDispatchHandler(opts) {
34
+ const realm = opts.realm ?? "billing-tools";
35
+ return async (request, ctx) => {
36
+ const { tool } = await ctx.params;
37
+ const authHeader = request.headers.get("authorization");
38
+ const rm = typeof opts.resourceMetadata === "function" ? opts.resourceMetadata(request) : opts.resourceMetadata;
39
+ return runWithAuth(authHeader, async () => {
40
+ try {
41
+ const body = await request.json().catch(() => ({}));
42
+ const result = await opts.dispatcher.dispatchTool(tool, body);
43
+ if (result && typeof result === "object" && result.status === "try_again_later") {
44
+ const retryAfter = result.retry_after_seconds;
45
+ return Response.json(result, {
46
+ status: 429,
47
+ headers: retryAfter ? { "Retry-After": String(retryAfter) } : undefined,
48
+ });
49
+ }
50
+ if (isUnauthorizedResult(result)) {
51
+ return Response.json(result, { status: 401, headers: { "WWW-Authenticate": wwwAuth(realm, rm) } });
52
+ }
53
+ return Response.json(result);
54
+ }
55
+ catch (err) {
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ if (/\bUnauthorized\b|\b401\b/i.test(message)) {
58
+ return Response.json({ error: message }, { status: 401, headers: { "WWW-Authenticate": wwwAuth(realm, rm) } });
59
+ }
60
+ const status = err instanceof ToolValidationError ? 400 : message.includes("Unknown tool") ? 404 : 500;
61
+ return Response.json({ error: message }, { status });
62
+ }
63
+ });
64
+ };
65
+ }
66
+ //# sourceMappingURL=rest.js.map