payweave 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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +272 -0
  3. package/dist/_tsup-dts-rollup.d.ts +19470 -0
  4. package/dist/chunk-4R5OGOCC.js +94 -0
  5. package/dist/chunk-4R5OGOCC.js.map +1 -0
  6. package/dist/chunk-535Q3GUN.js +175 -0
  7. package/dist/chunk-535Q3GUN.js.map +1 -0
  8. package/dist/chunk-67NRYGRC.js +192 -0
  9. package/dist/chunk-67NRYGRC.js.map +1 -0
  10. package/dist/chunk-CGQ73I4I.js +568 -0
  11. package/dist/chunk-CGQ73I4I.js.map +1 -0
  12. package/dist/chunk-CJEYDHY6.js +18 -0
  13. package/dist/chunk-CJEYDHY6.js.map +1 -0
  14. package/dist/chunk-CR63JAAS.js +3 -0
  15. package/dist/chunk-CR63JAAS.js.map +1 -0
  16. package/dist/chunk-E633MKVX.js +1507 -0
  17. package/dist/chunk-E633MKVX.js.map +1 -0
  18. package/dist/chunk-FBPOCXRZ.js +1135 -0
  19. package/dist/chunk-FBPOCXRZ.js.map +1 -0
  20. package/dist/chunk-HTRGMZFH.js +267 -0
  21. package/dist/chunk-HTRGMZFH.js.map +1 -0
  22. package/dist/chunk-IUVGM3K7.js +765 -0
  23. package/dist/chunk-IUVGM3K7.js.map +1 -0
  24. package/dist/chunk-JD4OZHRI.js +326 -0
  25. package/dist/chunk-JD4OZHRI.js.map +1 -0
  26. package/dist/chunk-NV3MZF7L.js +139 -0
  27. package/dist/chunk-NV3MZF7L.js.map +1 -0
  28. package/dist/chunk-NXQBP35U.js +13 -0
  29. package/dist/chunk-NXQBP35U.js.map +1 -0
  30. package/dist/chunk-QJHXDZYC.js +412 -0
  31. package/dist/chunk-QJHXDZYC.js.map +1 -0
  32. package/dist/chunk-TWZK62TZ.js +83 -0
  33. package/dist/chunk-TWZK62TZ.js.map +1 -0
  34. package/dist/cli/index.js +35381 -0
  35. package/dist/core/index.d.ts +85 -0
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -0
  38. package/dist/db/drizzle/index.d.ts +6 -0
  39. package/dist/db/drizzle/index.js +1532 -0
  40. package/dist/db/drizzle/index.js.map +1 -0
  41. package/dist/db/index.d.ts +43 -0
  42. package/dist/db/index.js +3 -0
  43. package/dist/db/index.js.map +1 -0
  44. package/dist/db/mongodb/index.d.ts +3 -0
  45. package/dist/db/mongodb/index.js +934 -0
  46. package/dist/db/mongodb/index.js.map +1 -0
  47. package/dist/db/mysql/index.d.ts +1 -0
  48. package/dist/db/mysql/index.js +12 -0
  49. package/dist/db/mysql/index.js.map +1 -0
  50. package/dist/db/postgres/index.d.ts +5 -0
  51. package/dist/db/postgres/index.js +791 -0
  52. package/dist/db/postgres/index.js.map +1 -0
  53. package/dist/db/prisma/index.d.ts +1 -0
  54. package/dist/db/prisma/index.js +12 -0
  55. package/dist/db/prisma/index.js.map +1 -0
  56. package/dist/db/sqlite/index.d.ts +5 -0
  57. package/dist/db/sqlite/index.js +750 -0
  58. package/dist/db/sqlite/index.js.map +1 -0
  59. package/dist/express/index.d.ts +1 -0
  60. package/dist/express/index.js +3 -0
  61. package/dist/express/index.js.map +1 -0
  62. package/dist/fastify/index.d.ts +1 -0
  63. package/dist/fastify/index.js +3 -0
  64. package/dist/fastify/index.js.map +1 -0
  65. package/dist/flutterwave/index.d.ts +27 -0
  66. package/dist/flutterwave/index.js +9 -0
  67. package/dist/flutterwave/index.js.map +1 -0
  68. package/dist/index.d.ts +85 -0
  69. package/dist/index.js +3677 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/next/index.d.ts +1 -0
  72. package/dist/next/index.js +3 -0
  73. package/dist/next/index.js.map +1 -0
  74. package/dist/paystack/index.d.ts +33 -0
  75. package/dist/paystack/index.js +9 -0
  76. package/dist/paystack/index.js.map +1 -0
  77. package/dist/products/index.d.ts +22 -0
  78. package/dist/products/index.js +7 -0
  79. package/dist/products/index.js.map +1 -0
  80. package/dist/testing/index.d.ts +11 -0
  81. package/dist/testing/index.js +69 -0
  82. package/dist/testing/index.js.map +1 -0
  83. package/dist/unified/index.d.ts +41 -0
  84. package/dist/unified/index.js +6 -0
  85. package/dist/unified/index.js.map +1 -0
  86. package/dist/webhooks/index.d.ts +8 -0
  87. package/dist/webhooks/index.js +8 -0
  88. package/dist/webhooks/index.js.map +1 -0
  89. package/package.json +164 -0
@@ -0,0 +1,139 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/db/schema.ts
4
+ var PW_TABLE_PREFIX = "pw_";
5
+ var PW_TABLES = {
6
+ customers: "pw_customers",
7
+ plans: "pw_plans",
8
+ subscriptions: "pw_subscriptions",
9
+ featureBalances: "pw_feature_balances",
10
+ webhookEvents: "pw_webhook_events",
11
+ migrations: "pw_migrations"
12
+ };
13
+ var pwIdSchema = z.string().regex(
14
+ /^pwv_[0-9A-HJKMNP-TV-Z]{26}$/i,
15
+ "expected a Payweave row id \u2014 'pwv_' followed by a 26-char ULID"
16
+ );
17
+ var pwSubscriptionStatusSchema = z.enum([
18
+ "active",
19
+ "past_due",
20
+ "canceled",
21
+ "incomplete",
22
+ "trialing"
23
+ ]);
24
+ var PW_ACTIVE_SUBSCRIPTION_STATUSES = ["active", "past_due", "trialing"];
25
+ var pwResetIntervalSchema = z.enum(["day", "week", "month", "year"]);
26
+ var pwPriceIntervalSchema = z.enum(["month", "year"]);
27
+ var DEFAULT_STALE_CLAIM_AFTER_MS = 6e4;
28
+ var pwCustomerSchema = z.object({
29
+ id: pwIdSchema,
30
+ /** Your `customerId` — unique across the table. */
31
+ externalId: z.string().min(1),
32
+ /** Column `provider_ids` (JSON), e.g. `{ stripe: "cus_x", paystack: "CUS_x" }`. */
33
+ providerIds: z.record(z.string().min(1), z.string().min(1)),
34
+ email: z.string().nullable(),
35
+ createdAt: z.date(),
36
+ updatedAt: z.date()
37
+ });
38
+ var pwCustomerUpsertSchema = z.object({
39
+ externalId: z.string().min(1),
40
+ email: z.string().optional()
41
+ });
42
+ var pwFeatureInclusionSchema = z.discriminatedUnion("type", [
43
+ z.looseObject({ type: z.literal("boolean") }),
44
+ z.looseObject({
45
+ type: z.literal("metered"),
46
+ limit: z.number().int().positive(),
47
+ reset: pwResetIntervalSchema
48
+ })
49
+ ]);
50
+ var pwPlanVersionSchema = z.object({
51
+ id: pwIdSchema,
52
+ /** The config-side plan id (`plan()`'s `id`), unique together with `version`. */
53
+ planId: z.string().min(1),
54
+ version: z.number().int().positive(),
55
+ group: z.string().min(1),
56
+ isDefault: z.boolean(),
57
+ name: z.string().nullable(),
58
+ /** Integer minor units — no floats ever (golden rule 7). `null` = free plan. */
59
+ priceMinor: z.number().int().nonnegative().nullable(),
60
+ priceCurrency: z.string().nullable(),
61
+ priceInterval: pwPriceIntervalSchema.nullable(),
62
+ /** Column `features` (JSON) — resolved includes, keyed by feature id. */
63
+ features: z.record(z.string().min(1), pwFeatureInclusionSchema),
64
+ /**
65
+ * Column `provider_refs` (JSON), e.g.
66
+ * `{ stripe: { productId, priceId }, paystack: { planCode } }` — filled in
67
+ * by billing sync (`payweave push`).
68
+ */
69
+ providerRefs: z.record(z.string().min(1), z.record(z.string().min(1), z.string().min(1))),
70
+ pushedAt: z.date()
71
+ });
72
+ var pwPlanVersionInputSchema = pwPlanVersionSchema.omit({
73
+ id: true,
74
+ version: true,
75
+ pushedAt: true
76
+ });
77
+ var pwSubscriptionSchema = z.object({
78
+ id: pwIdSchema,
79
+ /** FK → `pw_customers.id` (the `pwv_` id, NOT your external id). */
80
+ customerId: pwIdSchema,
81
+ planId: z.string().min(1),
82
+ planVersion: z.number().int().positive(),
83
+ group: z.string().min(1),
84
+ status: pwSubscriptionStatusSchema,
85
+ provider: z.string().min(1).nullable(),
86
+ providerSubscriptionRef: z.string().min(1).nullable(),
87
+ currentPeriodStart: z.date(),
88
+ currentPeriodEnd: z.date(),
89
+ cancelAtPeriodEnd: z.boolean(),
90
+ createdAt: z.date(),
91
+ updatedAt: z.date()
92
+ });
93
+ var pwSubscriptionInputSchema = pwSubscriptionSchema.omit({
94
+ id: true,
95
+ createdAt: true,
96
+ updatedAt: true
97
+ });
98
+ var pwSubscriptionPatchSchema = pwSubscriptionInputSchema.omit({ customerId: true, group: true }).partial();
99
+ var pwFeatureBalanceSchema = z.object({
100
+ id: pwIdSchema,
101
+ /** FK → `pw_customers.id`. */
102
+ customerId: pwIdSchema,
103
+ featureId: z.string().min(1),
104
+ group: z.string().min(1),
105
+ used: z.number().int(),
106
+ limit: z.number().int().nonnegative(),
107
+ resetInterval: pwResetIntervalSchema,
108
+ /** Billing-cycle anchor — the origin of ALL period arithmetic. */
109
+ anchor: z.date(),
110
+ periodStart: z.date(),
111
+ periodEnd: z.date(),
112
+ planId: z.string().min(1),
113
+ planVersion: z.number().int().positive(),
114
+ updatedAt: z.date()
115
+ });
116
+ var pwFeatureBalanceInitSchema = z.object({
117
+ limit: z.number().int().nonnegative(),
118
+ resetInterval: pwResetIntervalSchema,
119
+ anchor: z.date(),
120
+ planId: z.string().min(1),
121
+ planVersion: z.number().int().positive()
122
+ });
123
+ var pwWebhookEventSchema = z.object({
124
+ dedupeKey: z.string().min(1),
125
+ provider: z.string().min(1),
126
+ type: z.string().min(1),
127
+ receivedAt: z.date(),
128
+ claimedAt: z.date().nullable(),
129
+ appliedAt: z.date().nullable()
130
+ });
131
+ var pwMigrationRecordSchema = z.object({
132
+ name: z.string().min(1),
133
+ appliedAt: z.date(),
134
+ checksum: z.string().min(1)
135
+ });
136
+
137
+ export { DEFAULT_STALE_CLAIM_AFTER_MS, PW_ACTIVE_SUBSCRIPTION_STATUSES, PW_TABLES, PW_TABLE_PREFIX, pwCustomerSchema, pwCustomerUpsertSchema, pwFeatureBalanceInitSchema, pwFeatureBalanceSchema, pwFeatureInclusionSchema, pwIdSchema, pwMigrationRecordSchema, pwPlanVersionInputSchema, pwPlanVersionSchema, pwPriceIntervalSchema, pwResetIntervalSchema, pwSubscriptionInputSchema, pwSubscriptionPatchSchema, pwSubscriptionSchema, pwSubscriptionStatusSchema, pwWebhookEventSchema };
138
+ //# sourceMappingURL=chunk-NV3MZF7L.js.map
139
+ //# sourceMappingURL=chunk-NV3MZF7L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/schema.ts"],"names":[],"mappings":"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","file":"chunk-NV3MZF7L.js","sourcesContent":["/**\n * Row schemas + storage constants for the Payweave database layer. These Zod\n * schemas ARE the logical storage contract — every row an adapter returns\n * must parse against them, and the conformance suite\n * (`test/db/conformance.ts`) enforces exactly that. Exact DDL per dialect\n * lives with each adapter, which owns the mapping from these camelCase field\n * names to snake_case column names (`externalId` ↔ `external_id`,\n * `periodEnd` ↔ `period_end`, …).\n *\n * Conventions (\"Key columns\" notation):\n * - Columns marked `?` are NULLABLE here (`.nullable()` — the\n * value is always present on the row, `null` when unset). Optional keys\n * (`?:`) appear only on INPUT shapes.\n * - No floats anywhere: money is integer minor units + currency code (golden\n * rule 7 applies to storage), counters/versions are integers.\n * - All timestamps are UTC `Date` instances.\n *\n * Re-exported from `./db/index.ts` as part of the public `payweave/db`\n * subpath.\n */\nimport { z } from \"zod\";\n\n// ── Table names ──────────────────────────────────────────────────────────────\n\n/** Every Payweave-owned table/collection is prefixed `pw_`. */\nexport const PW_TABLE_PREFIX = \"pw_\";\n\n/**\n * Canonical table/collection names, keyed by the adapter store that owns\n * them. Adapters and migrations MUST use these constants — never\n * re-spell the names.\n */\nexport const PW_TABLES = {\n customers: \"pw_customers\",\n plans: \"pw_plans\",\n subscriptions: \"pw_subscriptions\",\n featureBalances: \"pw_feature_balances\",\n webhookEvents: \"pw_webhook_events\",\n migrations: \"pw_migrations\",\n} as const;\n\n/** Union of the canonical `pw_*` table names. */\nexport type PwTableName = (typeof PW_TABLES)[keyof typeof PW_TABLES];\n\n// ── Shared scalars ───────────────────────────────────────────────────────────\n\n/**\n * Payweave row id: `pwv_<ulid>` strings, unless noted otherwise below. ULID =\n * 26 chars of Crockford base32; decoding is case-insensitive, so both cases\n * are accepted. The exceptions are `pw_webhook_events.dedupe_key` and\n * `pw_migrations.name`, which are natural keys.\n */\nexport const pwIdSchema = z\n .string()\n .regex(\n /^pwv_[0-9A-HJKMNP-TV-Z]{26}$/i,\n \"expected a Payweave row id — 'pwv_' followed by a 26-char ULID\",\n );\n\n/**\n * Subscription lifecycle states (`pw_subscriptions.status`).\n */\nexport const pwSubscriptionStatusSchema = z.enum([\n \"active\",\n \"past_due\",\n \"canceled\",\n \"incomplete\",\n \"trialing\",\n]);\nexport type PwSubscriptionStatus = z.infer<typeof pwSubscriptionStatusSchema>;\n\n/**\n * Statuses covered by the partial unique index on (`customer_id`, `group`) —\n * at most ONE subscription per customer/group may be in any of these states\n * `subscriptions.getActive` answers from this set.\n */\nexport const PW_ACTIVE_SUBSCRIPTION_STATUSES = [\"active\", \"past_due\", \"trialing\"] as const;\n\n/** Metered-feature reset interval. */\nexport const pwResetIntervalSchema = z.enum([\"day\", \"week\", \"month\", \"year\"]);\nexport type PwResetInterval = z.infer<typeof pwResetIntervalSchema>;\n\n/** Plan price billing interval (`month` or `year`). */\nexport const pwPriceIntervalSchema = z.enum([\"month\", \"year\"]);\nexport type PwPriceInterval = z.infer<typeof pwPriceIntervalSchema>;\n\n/**\n * Default stale-claim window for `webhookEvents.claim`: a\n * claimed-but-unapplied event becomes re-claimable once\n * `now - claimed_at >= staleClaimAfterMs`, so a crashed apply is retried on\n * provider redelivery instead of silently dropped.\n */\nexport const DEFAULT_STALE_CLAIM_AFTER_MS = 60_000;\n\n// ── pw_customers ─────────────────────────────────────────────────────────────\n\n/**\n * `pw_customers` row — maps YOUR user id (`externalId`, unique) to\n * per-provider customer ids.\n */\nexport const pwCustomerSchema = z.object({\n id: pwIdSchema,\n /** Your `customerId` — unique across the table. */\n externalId: z.string().min(1),\n /** Column `provider_ids` (JSON), e.g. `{ stripe: \"cus_x\", paystack: \"CUS_x\" }`. */\n providerIds: z.record(z.string().min(1), z.string().min(1)),\n email: z.string().nullable(),\n createdAt: z.date(),\n updatedAt: z.date(),\n});\nexport type PwCustomer = z.infer<typeof pwCustomerSchema>;\n\n/**\n * Input for `customers.upsert` — keyed by `externalId`; provider refs are\n * linked separately via `customers.linkProviderRef`.\n */\nexport const pwCustomerUpsertSchema = z.object({\n externalId: z.string().min(1),\n email: z.string().optional(),\n});\nexport type PwCustomerUpsert = z.infer<typeof pwCustomerUpsertSchema>;\n\n// ── pw_plans ─────────────────────────────────────────────────────────────────\n\n/**\n * One resolved feature inclusion inside a plan version's `features` JSON\n * column. Loose objects, by design — unknown keys must survive a round-trip\n * through storage, since future callers may enrich inclusions with extra\n * fields.\n */\nexport const pwFeatureInclusionSchema = z.discriminatedUnion(\"type\", [\n z.looseObject({ type: z.literal(\"boolean\") }),\n z.looseObject({\n type: z.literal(\"metered\"),\n limit: z.number().int().positive(),\n reset: pwResetIntervalSchema,\n }),\n]);\nexport type PwFeatureInclusion = z.infer<typeof pwFeatureInclusionSchema>;\n\n/**\n * `pw_plans` row — an IMMUTABLE plan version pushed from config.\n * Append-only: `plans.pushVersion` never mutates or deletes\n * an existing (`planId`, `version`) row; running app instances keep reading\n * the version they were deployed with.\n */\nexport const pwPlanVersionSchema = z.object({\n id: pwIdSchema,\n /** The config-side plan id (`plan()`'s `id`), unique together with `version`. */\n planId: z.string().min(1),\n version: z.number().int().positive(),\n group: z.string().min(1),\n isDefault: z.boolean(),\n name: z.string().nullable(),\n /** Integer minor units — no floats ever (golden rule 7). `null` = free plan. */\n priceMinor: z.number().int().nonnegative().nullable(),\n priceCurrency: z.string().nullable(),\n priceInterval: pwPriceIntervalSchema.nullable(),\n /** Column `features` (JSON) — resolved includes, keyed by feature id. */\n features: z.record(z.string().min(1), pwFeatureInclusionSchema),\n /**\n * Column `provider_refs` (JSON), e.g.\n * `{ stripe: { productId, priceId }, paystack: { planCode } }` — filled in\n * by billing sync (`payweave push`).\n */\n providerRefs: z.record(z.string().min(1), z.record(z.string().min(1), z.string().min(1))),\n pushedAt: z.date(),\n});\nexport type PwPlanVersion = z.infer<typeof pwPlanVersionSchema>;\n\n/**\n * Input for `plans.pushVersion` — the adapter assigns `id`, the next\n * `version`, and `pushedAt` (append-only versioning).\n */\nexport const pwPlanVersionInputSchema = pwPlanVersionSchema.omit({\n id: true,\n version: true,\n pushedAt: true,\n});\nexport type PwPlanVersionInput = z.infer<typeof pwPlanVersionInputSchema>;\n\n// ── pw_subscriptions ─────────────────────────────────────────────────────────\n\n/**\n * `pw_subscriptions` row — one active row per (customer, group).\n * The default plan produces NO subscription row; \"customer\n * is on `free`\" is computed at read time.\n *\n * `provider` and `providerSubscriptionRef` are nullable: a free\n * (non-default) plan is \"recorded locally, no provider object\", so a local\n * activation has no provider ref to store.\n */\nexport const pwSubscriptionSchema = z.object({\n id: pwIdSchema,\n /** FK → `pw_customers.id` (the `pwv_` id, NOT your external id). */\n customerId: pwIdSchema,\n planId: z.string().min(1),\n planVersion: z.number().int().positive(),\n group: z.string().min(1),\n status: pwSubscriptionStatusSchema,\n provider: z.string().min(1).nullable(),\n providerSubscriptionRef: z.string().min(1).nullable(),\n currentPeriodStart: z.date(),\n currentPeriodEnd: z.date(),\n cancelAtPeriodEnd: z.boolean(),\n createdAt: z.date(),\n updatedAt: z.date(),\n});\nexport type PwSubscription = z.infer<typeof pwSubscriptionSchema>;\n\n/** Input for `subscriptions.create` — the adapter assigns `id` + timestamps. */\nexport const pwSubscriptionInputSchema = pwSubscriptionSchema.omit({\n id: true,\n createdAt: true,\n updatedAt: true,\n});\nexport type PwSubscriptionInput = z.infer<typeof pwSubscriptionInputSchema>;\n\n/**\n * Patch for `subscriptions.update`. A subscription never moves across\n * customers or groups — plan changes patch `planId`/`planVersion`/`status`\n * and period bounds on the SAME row.\n */\nexport const pwSubscriptionPatchSchema = pwSubscriptionInputSchema\n .omit({ customerId: true, group: true })\n .partial();\nexport type PwSubscriptionPatch = z.infer<typeof pwSubscriptionPatchSchema>;\n\n// ── pw_feature_balances ──────────────────────────────────────────────────────\n\n/**\n * `pw_feature_balances` row — metered usage state with lazy reset. Unique\n * per (`customerId`, `featureId`, `group`).\n *\n * `used` is a plain integer, NOT non-negative: unconditional `consume` (the\n * `report` path) may push a balance below zero by design. All period math\n * derives from `anchor` — never from a previously computed (clamped)\n * `periodStart`.\n */\nexport const pwFeatureBalanceSchema = z.object({\n id: pwIdSchema,\n /** FK → `pw_customers.id`. */\n customerId: pwIdSchema,\n featureId: z.string().min(1),\n group: z.string().min(1),\n used: z.number().int(),\n limit: z.number().int().nonnegative(),\n resetInterval: pwResetIntervalSchema,\n /** Billing-cycle anchor — the origin of ALL period arithmetic. */\n anchor: z.date(),\n periodStart: z.date(),\n periodEnd: z.date(),\n planId: z.string().min(1),\n planVersion: z.number().int().positive(),\n updatedAt: z.date(),\n});\nexport type PwFeatureBalance = z.infer<typeof pwFeatureBalanceSchema>;\n\n/**\n * Row template for `balances.consume`'s lazy creation and for\n * `balances.resetTo` (plan changes). Only the underivable fields: the adapter\n * derives `used` (starts at 0), the current `periodStart`/`periodEnd` (from\n * `anchor` + `resetInterval` + `now`), and `updatedAt`.\n */\nexport const pwFeatureBalanceInitSchema = z.object({\n limit: z.number().int().nonnegative(),\n resetInterval: pwResetIntervalSchema,\n anchor: z.date(),\n planId: z.string().min(1),\n planVersion: z.number().int().positive(),\n});\nexport type PwFeatureBalanceInit = z.infer<typeof pwFeatureBalanceInitSchema>;\n\n// ── pw_webhook_events ────────────────────────────────────────────────────────\n\n/**\n * `pw_webhook_events` row — the idempotency gate for `event.apply()`.\n * `dedupeKey` is the primary key (a natural key, not a\n * `pwv_` id); `claimedAt`/`appliedAt` drive the once-only + stale-claim\n * semantics of `webhookEvents.claim`.\n */\nexport const pwWebhookEventSchema = z.object({\n dedupeKey: z.string().min(1),\n provider: z.string().min(1),\n type: z.string().min(1),\n receivedAt: z.date(),\n claimedAt: z.date().nullable(),\n appliedAt: z.date().nullable(),\n});\nexport type PwWebhookEvent = z.infer<typeof pwWebhookEventSchema>;\n\n// ── pw_migrations ────────────────────────────────────────────────────────────\n\n/**\n * `pw_migrations` row — migration ledger for SQL adapters.\n * `name` is the primary key; a checksum mismatch on an applied migration\n * fails loudly (never re-run mutated history).\n */\nexport const pwMigrationRecordSchema = z.object({\n name: z.string().min(1),\n appliedAt: z.date(),\n checksum: z.string().min(1),\n});\nexport type PwMigrationRecord = z.infer<typeof pwMigrationRecordSchema>;\n"]}
@@ -0,0 +1,13 @@
1
+ import { createHmac, timingSafeEqual } from 'crypto';
2
+
3
+ // src/webhooks/paystack.ts
4
+ function verifyPaystack(rawBody, signatureHeader, secret) {
5
+ const digest = createHmac("sha512", secret).update(rawBody).digest("hex");
6
+ const expected = Buffer.from(digest, "utf8");
7
+ const received = Buffer.from(signatureHeader ?? "", "utf8");
8
+ return expected.length === received.length && timingSafeEqual(expected, received);
9
+ }
10
+
11
+ export { verifyPaystack };
12
+ //# sourceMappingURL=chunk-NXQBP35U.js.map
13
+ //# sourceMappingURL=chunk-NXQBP35U.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webhooks/paystack.ts"],"names":[],"mappings":";;;AAeO,SAAS,cAAA,CACd,OAAA,EACA,eAAA,EACA,MAAA,EACS;AACT,EAAA,MAAM,MAAA,GAAS,WAAW,QAAA,EAAU,MAAM,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA;AACxE,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,IAAA,CAAK,MAAA,EAAQ,MAAM,CAAA;AAC3C,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,IAAA,CAAK,eAAA,IAAmB,IAAI,MAAM,CAAA;AAC1D,EAAA,OAAO,SAAS,MAAA,KAAW,QAAA,CAAS,MAAA,IAAU,eAAA,CAAgB,UAAU,QAAQ,CAAA;AAClF","file":"chunk-NXQBP35U.js","sourcesContent":["/**\n * Paystack webhook verification. HMAC-SHA512 over the RAW request\n * body, hex digest, keyed with the API secret key. Timing-safe with a\n * length-guard; fails closed on a missing/short header.\n *\n * Docs: https://paystack.com/docs/payments/webhooks/#verify-event-origin\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/**\n * @param rawBody - Exact received bytes (never a re-serialized object).\n * @param signatureHeader - Value of the `x-paystack-signature` header.\n * @param secret - Paystack API secret key (`sk_live_*` / `sk_test_*`).\n * @returns `true` iff the signature matches; `false` otherwise (never throws).\n */\nexport function verifyPaystack(\n rawBody: string | Uint8Array,\n signatureHeader: string | null | undefined,\n secret: string,\n): boolean {\n const digest = createHmac(\"sha512\", secret).update(rawBody).digest(\"hex\");\n const expected = Buffer.from(digest, \"utf8\");\n const received = Buffer.from(signatureHeader ?? \"\", \"utf8\");\n return expected.length === received.length && timingSafeEqual(expected, received);\n}\n"]}
@@ -0,0 +1,412 @@
1
+ import { PW_TABLES, PW_ACTIVE_SUBSCRIPTION_STATUSES } from './chunk-NV3MZF7L.js';
2
+ import { PayweaveError } from './chunk-HTRGMZFH.js';
3
+ import { createHash } from 'crypto';
4
+
5
+ // src/db/migrations/errors.ts
6
+ var PayweaveMigrationError = class extends PayweaveError {
7
+ constructor(message, options = {}) {
8
+ super(message, { isRetryable: false, ...options });
9
+ this.name = "PayweaveMigrationError";
10
+ }
11
+ };
12
+ var PayweaveMigrationHistoryError = class extends PayweaveMigrationError {
13
+ /** The ledger `name` (= embedded migration `id`) that failed verification. */
14
+ migrationId;
15
+ reason;
16
+ constructor(message, details, options = {}) {
17
+ super(message, options);
18
+ this.name = "PayweaveMigrationHistoryError";
19
+ this.migrationId = details.migrationId;
20
+ this.reason = details.reason;
21
+ }
22
+ toJSON() {
23
+ return { ...super.toJSON(), migrationId: this.migrationId, reason: this.reason };
24
+ }
25
+ };
26
+
27
+ // src/db/migrations/ledger.ts
28
+ var MIGRATION_LEDGER_TABLE = PW_TABLES.migrations;
29
+ function ledgerEnsureSql(dialect) {
30
+ switch (dialect) {
31
+ case "postgres":
32
+ return `CREATE TABLE IF NOT EXISTS ${MIGRATION_LEDGER_TABLE} (
33
+ name TEXT PRIMARY KEY,
34
+ applied_at TIMESTAMPTZ NOT NULL,
35
+ checksum TEXT NOT NULL
36
+ )`;
37
+ case "mysql":
38
+ return `CREATE TABLE IF NOT EXISTS ${MIGRATION_LEDGER_TABLE} (
39
+ name VARCHAR(255) NOT NULL,
40
+ applied_at DATETIME(3) NOT NULL,
41
+ checksum VARCHAR(255) NOT NULL,
42
+ PRIMARY KEY (name)
43
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`;
44
+ case "sqlite":
45
+ return `CREATE TABLE IF NOT EXISTS ${MIGRATION_LEDGER_TABLE} (
46
+ name TEXT PRIMARY KEY,
47
+ applied_at INTEGER NOT NULL,
48
+ checksum TEXT NOT NULL
49
+ )`;
50
+ }
51
+ }
52
+ function ledgerSelectSql(dialect) {
53
+ return `SELECT name, checksum FROM ${MIGRATION_LEDGER_TABLE} ORDER BY name`;
54
+ }
55
+ function ledgerInsertSql(dialect) {
56
+ const placeholders = dialect === "postgres" ? "$1, $2, $3" : "?, ?, ?";
57
+ return `INSERT INTO ${MIGRATION_LEDGER_TABLE} (name, applied_at, checksum) VALUES (${placeholders})`;
58
+ }
59
+ function ledgerInsertParams(dialect, migrationId, checksum, appliedAt) {
60
+ return [migrationId, dialect === "sqlite" ? appliedAt.getTime() : appliedAt, checksum];
61
+ }
62
+
63
+ // src/db/migrations/ddl.ts
64
+ var ACTIVE_STATUS_SQL_LIST = PW_ACTIVE_SUBSCRIPTION_STATUSES.map((s) => `'${s}'`).join(", ");
65
+ var freezeSql = (statements) => Object.freeze([...statements]);
66
+ var POSTGRES_INIT_STATEMENTS = freezeSql([
67
+ ledgerEnsureSql("postgres"),
68
+ `CREATE TABLE ${PW_TABLES.customers} (
69
+ id TEXT PRIMARY KEY,
70
+ external_id TEXT NOT NULL,
71
+ provider_ids JSONB NOT NULL,
72
+ email TEXT,
73
+ created_at TIMESTAMPTZ NOT NULL,
74
+ updated_at TIMESTAMPTZ NOT NULL,
75
+ CONSTRAINT pw_customers_external_id_uq UNIQUE (external_id)
76
+ )`,
77
+ `CREATE TABLE ${PW_TABLES.plans} (
78
+ id TEXT PRIMARY KEY,
79
+ plan_id TEXT NOT NULL,
80
+ version INTEGER NOT NULL,
81
+ "group" TEXT NOT NULL,
82
+ is_default BOOLEAN NOT NULL,
83
+ name TEXT,
84
+ price_minor BIGINT,
85
+ price_currency TEXT,
86
+ price_interval TEXT,
87
+ features JSONB NOT NULL,
88
+ provider_refs JSONB NOT NULL,
89
+ pushed_at TIMESTAMPTZ NOT NULL,
90
+ CONSTRAINT pw_plans_plan_id_version_uq UNIQUE (plan_id, version)
91
+ )`,
92
+ `CREATE TABLE ${PW_TABLES.subscriptions} (
93
+ id TEXT PRIMARY KEY,
94
+ customer_id TEXT NOT NULL,
95
+ plan_id TEXT NOT NULL,
96
+ plan_version INTEGER NOT NULL,
97
+ "group" TEXT NOT NULL,
98
+ status TEXT NOT NULL,
99
+ provider TEXT,
100
+ provider_subscription_ref TEXT,
101
+ current_period_start TIMESTAMPTZ NOT NULL,
102
+ current_period_end TIMESTAMPTZ NOT NULL,
103
+ cancel_at_period_end BOOLEAN NOT NULL,
104
+ created_at TIMESTAMPTZ NOT NULL,
105
+ updated_at TIMESTAMPTZ NOT NULL,
106
+ CONSTRAINT pw_subscriptions_customer_id_fk FOREIGN KEY (customer_id) REFERENCES ${PW_TABLES.customers} (id)
107
+ )`,
108
+ `CREATE UNIQUE INDEX pw_subscriptions_active_uq
109
+ ON ${PW_TABLES.subscriptions} (customer_id, "group")
110
+ WHERE status IN (${ACTIVE_STATUS_SQL_LIST})`,
111
+ `CREATE TABLE ${PW_TABLES.featureBalances} (
112
+ id TEXT PRIMARY KEY,
113
+ customer_id TEXT NOT NULL,
114
+ feature_id TEXT NOT NULL,
115
+ "group" TEXT NOT NULL,
116
+ used BIGINT NOT NULL,
117
+ "limit" BIGINT NOT NULL,
118
+ reset_interval TEXT NOT NULL,
119
+ anchor TIMESTAMPTZ NOT NULL,
120
+ period_start TIMESTAMPTZ NOT NULL,
121
+ period_end TIMESTAMPTZ NOT NULL,
122
+ plan_id TEXT NOT NULL,
123
+ plan_version INTEGER NOT NULL,
124
+ updated_at TIMESTAMPTZ NOT NULL,
125
+ CONSTRAINT pw_feature_balances_customer_id_fk FOREIGN KEY (customer_id) REFERENCES ${PW_TABLES.customers} (id),
126
+ CONSTRAINT pw_feature_balances_customer_feature_group_uq UNIQUE (customer_id, feature_id, "group")
127
+ )`,
128
+ `CREATE TABLE ${PW_TABLES.webhookEvents} (
129
+ dedupe_key TEXT PRIMARY KEY,
130
+ provider TEXT NOT NULL,
131
+ type TEXT NOT NULL,
132
+ received_at TIMESTAMPTZ NOT NULL,
133
+ claimed_at TIMESTAMPTZ,
134
+ applied_at TIMESTAMPTZ
135
+ )`
136
+ ]);
137
+ var MYSQL_INIT_STATEMENTS = freezeSql([
138
+ ledgerEnsureSql("mysql"),
139
+ `CREATE TABLE ${PW_TABLES.customers} (
140
+ id VARCHAR(255) NOT NULL,
141
+ external_id VARCHAR(255) NOT NULL,
142
+ provider_ids JSON NOT NULL,
143
+ email VARCHAR(255) NULL,
144
+ created_at DATETIME(3) NOT NULL,
145
+ updated_at DATETIME(3) NOT NULL,
146
+ PRIMARY KEY (id),
147
+ CONSTRAINT pw_customers_external_id_uq UNIQUE (external_id)
148
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`,
149
+ `CREATE TABLE ${PW_TABLES.plans} (
150
+ id VARCHAR(255) NOT NULL,
151
+ plan_id VARCHAR(255) NOT NULL,
152
+ version INT NOT NULL,
153
+ \`group\` VARCHAR(255) NOT NULL,
154
+ is_default TINYINT(1) NOT NULL,
155
+ name VARCHAR(255) NULL,
156
+ price_minor BIGINT NULL,
157
+ price_currency VARCHAR(255) NULL,
158
+ price_interval VARCHAR(255) NULL,
159
+ features JSON NOT NULL,
160
+ provider_refs JSON NOT NULL,
161
+ pushed_at DATETIME(3) NOT NULL,
162
+ PRIMARY KEY (id),
163
+ CONSTRAINT pw_plans_plan_id_version_uq UNIQUE (plan_id, version)
164
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`,
165
+ `CREATE TABLE ${PW_TABLES.subscriptions} (
166
+ id VARCHAR(255) NOT NULL,
167
+ customer_id VARCHAR(255) NOT NULL,
168
+ plan_id VARCHAR(255) NOT NULL,
169
+ plan_version INT NOT NULL,
170
+ \`group\` VARCHAR(255) NOT NULL,
171
+ status VARCHAR(255) NOT NULL,
172
+ provider VARCHAR(255) NULL,
173
+ provider_subscription_ref VARCHAR(255) NULL,
174
+ current_period_start DATETIME(3) NOT NULL,
175
+ current_period_end DATETIME(3) NOT NULL,
176
+ cancel_at_period_end TINYINT(1) NOT NULL,
177
+ created_at DATETIME(3) NOT NULL,
178
+ updated_at DATETIME(3) NOT NULL,
179
+ active_slot CHAR(1) GENERATED ALWAYS AS (
180
+ CASE WHEN status IN (${ACTIVE_STATUS_SQL_LIST}) THEN 'x' ELSE NULL END
181
+ ) STORED,
182
+ PRIMARY KEY (id),
183
+ CONSTRAINT pw_subscriptions_customer_id_fk FOREIGN KEY (customer_id) REFERENCES ${PW_TABLES.customers} (id),
184
+ CONSTRAINT pw_subscriptions_active_uq UNIQUE (customer_id, \`group\`, active_slot)
185
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`,
186
+ `CREATE TABLE ${PW_TABLES.featureBalances} (
187
+ id VARCHAR(255) NOT NULL,
188
+ customer_id VARCHAR(255) NOT NULL,
189
+ feature_id VARCHAR(255) NOT NULL,
190
+ \`group\` VARCHAR(255) NOT NULL,
191
+ used BIGINT NOT NULL,
192
+ \`limit\` BIGINT NOT NULL,
193
+ reset_interval VARCHAR(255) NOT NULL,
194
+ anchor DATETIME(3) NOT NULL,
195
+ period_start DATETIME(3) NOT NULL,
196
+ period_end DATETIME(3) NOT NULL,
197
+ plan_id VARCHAR(255) NOT NULL,
198
+ plan_version INT NOT NULL,
199
+ updated_at DATETIME(3) NOT NULL,
200
+ PRIMARY KEY (id),
201
+ CONSTRAINT pw_feature_balances_customer_id_fk FOREIGN KEY (customer_id) REFERENCES ${PW_TABLES.customers} (id),
202
+ CONSTRAINT pw_feature_balances_customer_feature_group_uq UNIQUE (customer_id, feature_id, \`group\`)
203
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`,
204
+ `CREATE TABLE ${PW_TABLES.webhookEvents} (
205
+ dedupe_key VARCHAR(255) NOT NULL,
206
+ provider VARCHAR(255) NOT NULL,
207
+ type VARCHAR(255) NOT NULL,
208
+ received_at DATETIME(3) NOT NULL,
209
+ claimed_at DATETIME(3) NULL,
210
+ applied_at DATETIME(3) NULL,
211
+ PRIMARY KEY (dedupe_key)
212
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`
213
+ ]);
214
+ var SQLITE_INIT_STATEMENTS = freezeSql([
215
+ ledgerEnsureSql("sqlite"),
216
+ `CREATE TABLE ${PW_TABLES.customers} (
217
+ id TEXT PRIMARY KEY,
218
+ external_id TEXT NOT NULL,
219
+ provider_ids TEXT NOT NULL,
220
+ email TEXT,
221
+ created_at INTEGER NOT NULL,
222
+ updated_at INTEGER NOT NULL,
223
+ CONSTRAINT pw_customers_external_id_uq UNIQUE (external_id)
224
+ )`,
225
+ `CREATE TABLE ${PW_TABLES.plans} (
226
+ id TEXT PRIMARY KEY,
227
+ plan_id TEXT NOT NULL,
228
+ version INTEGER NOT NULL,
229
+ "group" TEXT NOT NULL,
230
+ is_default INTEGER NOT NULL CHECK (is_default IN (0, 1)),
231
+ name TEXT,
232
+ price_minor INTEGER,
233
+ price_currency TEXT,
234
+ price_interval TEXT,
235
+ features TEXT NOT NULL,
236
+ provider_refs TEXT NOT NULL,
237
+ pushed_at INTEGER NOT NULL,
238
+ CONSTRAINT pw_plans_plan_id_version_uq UNIQUE (plan_id, version)
239
+ )`,
240
+ `CREATE TABLE ${PW_TABLES.subscriptions} (
241
+ id TEXT PRIMARY KEY,
242
+ customer_id TEXT NOT NULL,
243
+ plan_id TEXT NOT NULL,
244
+ plan_version INTEGER NOT NULL,
245
+ "group" TEXT NOT NULL,
246
+ status TEXT NOT NULL,
247
+ provider TEXT,
248
+ provider_subscription_ref TEXT,
249
+ current_period_start INTEGER NOT NULL,
250
+ current_period_end INTEGER NOT NULL,
251
+ cancel_at_period_end INTEGER NOT NULL CHECK (cancel_at_period_end IN (0, 1)),
252
+ created_at INTEGER NOT NULL,
253
+ updated_at INTEGER NOT NULL,
254
+ CONSTRAINT pw_subscriptions_customer_id_fk FOREIGN KEY (customer_id) REFERENCES ${PW_TABLES.customers} (id)
255
+ )`,
256
+ `CREATE UNIQUE INDEX pw_subscriptions_active_uq
257
+ ON ${PW_TABLES.subscriptions} (customer_id, "group")
258
+ WHERE status IN (${ACTIVE_STATUS_SQL_LIST})`,
259
+ `CREATE TABLE ${PW_TABLES.featureBalances} (
260
+ id TEXT PRIMARY KEY,
261
+ customer_id TEXT NOT NULL,
262
+ feature_id TEXT NOT NULL,
263
+ "group" TEXT NOT NULL,
264
+ used INTEGER NOT NULL,
265
+ "limit" INTEGER NOT NULL,
266
+ reset_interval TEXT NOT NULL,
267
+ anchor INTEGER NOT NULL,
268
+ period_start INTEGER NOT NULL,
269
+ period_end INTEGER NOT NULL,
270
+ plan_id TEXT NOT NULL,
271
+ plan_version INTEGER NOT NULL,
272
+ updated_at INTEGER NOT NULL,
273
+ CONSTRAINT pw_feature_balances_customer_id_fk FOREIGN KEY (customer_id) REFERENCES ${PW_TABLES.customers} (id),
274
+ CONSTRAINT pw_feature_balances_customer_feature_group_uq UNIQUE (customer_id, feature_id, "group")
275
+ )`,
276
+ `CREATE TABLE ${PW_TABLES.webhookEvents} (
277
+ dedupe_key TEXT PRIMARY KEY,
278
+ provider TEXT NOT NULL,
279
+ type TEXT NOT NULL,
280
+ received_at INTEGER NOT NULL,
281
+ claimed_at INTEGER,
282
+ applied_at INTEGER
283
+ )`
284
+ ]);
285
+
286
+ // src/db/migrations/definitions.ts
287
+ var migration = (id, name, statements) => Object.freeze({ id, name, statements });
288
+ var MIGRATIONS_BY_DIALECT = {
289
+ postgres: Object.freeze([migration("0001_init", "init", POSTGRES_INIT_STATEMENTS)]),
290
+ mysql: Object.freeze([migration("0001_init", "init", MYSQL_INIT_STATEMENTS)]),
291
+ sqlite: Object.freeze([migration("0001_init", "init", SQLITE_INIT_STATEMENTS)])
292
+ };
293
+ function migrationsFor(dialect) {
294
+ switch (dialect) {
295
+ case "postgres":
296
+ case "mysql":
297
+ case "sqlite":
298
+ return MIGRATIONS_BY_DIALECT[dialect];
299
+ default:
300
+ throw new PayweaveMigrationError(
301
+ `Unsupported SQL migration dialect ${JSON.stringify(dialect)} \u2014 the Payweave migrations engine covers "postgres", "mysql" and "sqlite" only (docs/v1/database.md \xA74: mongodb/prisma/drizzle have no Payweave-owned SQL migrations).`
302
+ );
303
+ }
304
+ }
305
+
306
+ // src/db/migrations/engine.ts
307
+ function migrationChecksum(migration2) {
308
+ const hash = createHash("sha256");
309
+ for (const statement of migration2.statements) {
310
+ hash.update(statement, "utf8");
311
+ hash.update("\0", "utf8");
312
+ }
313
+ return `sha256:${hash.digest("hex")}`;
314
+ }
315
+ function normalizeMigrationSet(migrations) {
316
+ const seen = /* @__PURE__ */ new Set();
317
+ for (const m of migrations) {
318
+ if (seen.has(m.id)) {
319
+ throw new PayweaveMigrationError(
320
+ `Malformed embedded migration set: duplicate migration id "${m.id}".`
321
+ );
322
+ }
323
+ seen.add(m.id);
324
+ if (m.statements.length === 0 || m.statements.some((s) => s.trim() === "")) {
325
+ throw new PayweaveMigrationError(
326
+ `Malformed embedded migration set: migration "${m.id}" has an empty statement list or a blank statement.`
327
+ );
328
+ }
329
+ }
330
+ return [...migrations].sort((a, b) => a.id < b.id ? -1 : 1);
331
+ }
332
+ async function readVerifiedLedger(executor, dialect, migrations) {
333
+ await executor.query(ledgerEnsureSql(dialect));
334
+ const { rows } = await executor.query(ledgerSelectSql());
335
+ const byId = new Map(migrations.map((m) => [m.id, m]));
336
+ const applied = /* @__PURE__ */ new Set();
337
+ for (const row of rows) {
338
+ const name = row["name"];
339
+ const checksum = row["checksum"];
340
+ if (typeof name !== "string" || typeof checksum !== "string") {
341
+ throw new PayweaveMigrationError(
342
+ `The pw_migrations ledger returned a malformed row (expected string "name" and "checksum" columns) \u2014 got ${JSON.stringify(row)}.`
343
+ );
344
+ }
345
+ const embedded = byId.get(name);
346
+ if (embedded === void 0) {
347
+ throw new PayweaveMigrationHistoryError(
348
+ `Payweave migration history mismatch: the pw_migrations ledger contains "${name}", which is not among the migrations embedded in this build. Refusing to plan or apply against an unknown migration history \u2014 this usually means the database was migrated by a newer Payweave version, or the ledger was edited by hand (docs/v1/database.md \xA74).`,
349
+ { migrationId: name, reason: "unknown-migration" }
350
+ );
351
+ }
352
+ const expected = migrationChecksum(embedded);
353
+ if (checksum !== expected) {
354
+ throw new PayweaveMigrationHistoryError(
355
+ `Payweave migration "${name}" was modified after it was applied: ledger checksum ${checksum} does not match this build's checksum ${expected}. Applied migrations are immutable \u2014 NEVER edit an applied migration; ship the change as a new migration instead (forward-only, docs/v1/database.md \xA74).`,
356
+ { migrationId: name, reason: "checksum-mismatch" }
357
+ );
358
+ }
359
+ applied.add(name);
360
+ }
361
+ return applied;
362
+ }
363
+ async function planMigrations(executor, dialect, migrations = migrationsFor(dialect)) {
364
+ const ordered = normalizeMigrationSet(migrations);
365
+ const appliedSet = await readVerifiedLedger(executor, dialect, ordered);
366
+ return {
367
+ pending: ordered.filter((m) => !appliedSet.has(m.id)).map((m) => m.id),
368
+ applied: ordered.filter((m) => appliedSet.has(m.id)).map((m) => m.id)
369
+ };
370
+ }
371
+ async function applyMigrations(executor, dialect, migrations = migrationsFor(dialect)) {
372
+ const ordered = normalizeMigrationSet(migrations);
373
+ const appliedSet = await readVerifiedLedger(executor, dialect, ordered);
374
+ const pending = ordered.filter((m) => !appliedSet.has(m.id));
375
+ const applied = [];
376
+ for (const migration2 of pending) {
377
+ const runOne = async (tx) => {
378
+ for (const [index, statement] of migration2.statements.entries()) {
379
+ try {
380
+ await tx.query(statement);
381
+ } catch (cause) {
382
+ throw new PayweaveMigrationError(
383
+ `Payweave migration "${migration2.id}" failed at statement ${index + 1} of ${migration2.statements.length}.`,
384
+ { cause }
385
+ );
386
+ }
387
+ }
388
+ try {
389
+ await tx.query(
390
+ ledgerInsertSql(dialect),
391
+ ledgerInsertParams(dialect, migration2.id, migrationChecksum(migration2), /* @__PURE__ */ new Date())
392
+ );
393
+ } catch (cause) {
394
+ throw new PayweaveMigrationError(
395
+ `Payweave migration "${migration2.id}" executed, but recording it in the pw_migrations ledger failed.`,
396
+ { cause }
397
+ );
398
+ }
399
+ };
400
+ if (executor.transaction) {
401
+ await executor.transaction(runOne);
402
+ } else {
403
+ await runOne(executor);
404
+ }
405
+ applied.push(migration2.id);
406
+ }
407
+ return { applied };
408
+ }
409
+
410
+ export { applyMigrations, planMigrations };
411
+ //# sourceMappingURL=chunk-QJHXDZYC.js.map
412
+ //# sourceMappingURL=chunk-QJHXDZYC.js.map