diff --git a/dev/apollo-federation/supergraph.graphql b/dev/apollo-federation/supergraph.graphql index 865c5fd87..baeee5a52 100644 --- a/dev/apollo-federation/supergraph.graphql +++ b/dev/apollo-federation/supergraph.graphql @@ -501,6 +501,29 @@ type CashoutOffer walletId: WalletId! } +type CashWalletCutover + @join__type(graph: PUBLIC) +{ + completedAt: Timestamp + cutoverVersion: Int! + pauseReason: String + pausedAt: Timestamp + runId: String + scheduledAt: Timestamp + startedAt: Timestamp + state: CashWalletCutoverState! + updatedAt: Timestamp! + updatedBy: String +} + +enum CashWalletCutoverState + @join__type(graph: PUBLIC) +{ + COMPLETE @join__enumValue(graph: PUBLIC) + IN_PROGRESS @join__enumValue(graph: PUBLIC) + PRE @join__enumValue(graph: PUBLIC) +} + """(Positive) Cent amount (1/100 of a dollar)""" scalar CentAmount @join__type(graph: PUBLIC) @@ -1651,6 +1674,7 @@ type Query btcPrice(currency: DisplayCurrency! = "USD"): Price @deprecated(reason: "Deprecated in favor of realtimePrice") btcPriceList(range: PriceGraphRange!): [PricePoint] businessMapMarkers: [MapMarker!]! + cashWalletCutover: CashWalletCutover! currencyList: [Currency!]! globals: Globals isFlashNpub(input: IsFlashNpubInput!): IsFlashNpubPayload diff --git a/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/01-query-admin-state.bru b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/01-query-admin-state.bru new file mode 100644 index 000000000..1cc27b2f2 --- /dev/null +++ b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/01-query-admin-state.bru @@ -0,0 +1,48 @@ +meta { + name: 01-query-admin-state + type: graphql + seq: 1 +} + +post { + url: {{admin_url}} + body: graphql + auth: bearer +} + +auth:bearer { + token: {{admin_token}} +} + +body:graphql { + query CashWalletCutoverAdminState { + cashWalletCutover { + state + scheduledAt + startedAt + completedAt + pausedAt + pauseReason + cutoverVersion + runId + updatedBy + updatedAt + } + } +} + +body:graphql:vars { + {} +} + +script:post-response { + test("admin cutover state returns without GraphQL errors", function () { + const jsonData = res.getBody() + expect(jsonData.errors).to.be.undefined + expect(jsonData.data.cashWalletCutover.state).to.be.oneOf([ + "PRE", + "IN_PROGRESS", + "COMPLETE", + ]) + }) +} diff --git a/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/02-set-scheduled-pre.bru b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/02-set-scheduled-pre.bru new file mode 100644 index 000000000..963c26430 --- /dev/null +++ b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/02-set-scheduled-pre.bru @@ -0,0 +1,61 @@ +meta { + name: 02-set-scheduled-pre + type: graphql + seq: 2 +} + +post { + url: {{admin_url}} + body: graphql + auth: bearer +} + +auth:bearer { + token: {{admin_token}} +} + +body:graphql { + mutation CashWalletCutoverSetScheduledPre($input: CashWalletCutoverUpdateInput!) { + cashWalletCutoverUpdate(input: $input) { + errors { + message + } + cashWalletCutover { + state + scheduledAt + startedAt + completedAt + pauseReason + cutoverVersion + runId + updatedBy + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "state": "PRE", + "scheduledAt": "2026-05-22T15:00:00.000Z", + "cutoverVersion": 345, + "runId": "manual-eng-345", + "pauseReason": "manual ENG-345 preflight" + } + } +} + +script:post-response { + test("sets cutover config to PRE with schedule metadata", function () { + const jsonData = res.getBody() + expect(jsonData.errors).to.be.undefined + expect(jsonData.data.cashWalletCutoverUpdate.errors).to.eql([]) + + const config = jsonData.data.cashWalletCutoverUpdate.cashWalletCutover + expect(config.state).to.eql("PRE") + expect(config.cutoverVersion).to.eql(345) + expect(config.runId).to.eql("manual-eng-345") + }) +} diff --git a/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/03-set-in-progress.bru b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/03-set-in-progress.bru new file mode 100644 index 000000000..e44b497d4 --- /dev/null +++ b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/03-set-in-progress.bru @@ -0,0 +1,60 @@ +meta { + name: 03-set-in-progress + type: graphql + seq: 3 +} + +post { + url: {{admin_url}} + body: graphql + auth: bearer +} + +auth:bearer { + token: {{admin_token}} +} + +body:graphql { + mutation CashWalletCutoverSetInProgress($input: CashWalletCutoverUpdateInput!) { + cashWalletCutoverUpdate(input: $input) { + errors { + message + } + cashWalletCutover { + state + scheduledAt + startedAt + completedAt + pauseReason + cutoverVersion + runId + updatedBy + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "state": "IN_PROGRESS", + "cutoverVersion": 345, + "runId": "manual-eng-345", + "pauseReason": "manual ENG-345 in-progress test" + } + } +} + +script:post-response { + test("sets cutover config to IN_PROGRESS", function () { + const jsonData = res.getBody() + expect(jsonData.errors).to.be.undefined + expect(jsonData.data.cashWalletCutoverUpdate.errors).to.eql([]) + + const config = jsonData.data.cashWalletCutoverUpdate.cashWalletCutover + expect(config.state).to.eql("IN_PROGRESS") + expect(config.cutoverVersion).to.eql(345) + expect(config.runId).to.eql("manual-eng-345") + }) +} diff --git a/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/04-set-complete.bru b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/04-set-complete.bru new file mode 100644 index 000000000..c8ab4e0f8 --- /dev/null +++ b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/04-set-complete.bru @@ -0,0 +1,60 @@ +meta { + name: 04-set-complete + type: graphql + seq: 4 +} + +post { + url: {{admin_url}} + body: graphql + auth: bearer +} + +auth:bearer { + token: {{admin_token}} +} + +body:graphql { + mutation CashWalletCutoverSetComplete($input: CashWalletCutoverUpdateInput!) { + cashWalletCutoverUpdate(input: $input) { + errors { + message + } + cashWalletCutover { + state + scheduledAt + startedAt + completedAt + pauseReason + cutoverVersion + runId + updatedBy + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "state": "COMPLETE", + "cutoverVersion": 345, + "runId": "manual-eng-345", + "pauseReason": "manual ENG-345 complete test" + } + } +} + +script:post-response { + test("sets cutover config to COMPLETE", function () { + const jsonData = res.getBody() + expect(jsonData.errors).to.be.undefined + expect(jsonData.data.cashWalletCutoverUpdate.errors).to.eql([]) + + const config = jsonData.data.cashWalletCutoverUpdate.cashWalletCutover + expect(config.state).to.eql("COMPLETE") + expect(config.cutoverVersion).to.eql(345) + expect(config.runId).to.eql("manual-eng-345") + }) +} diff --git a/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/folder.bru b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/folder.bru new file mode 100644 index 000000000..c180d00b2 --- /dev/null +++ b/dev/bruno/Flash GraphQL API/admin/cash-wallet-cutover/folder.bru @@ -0,0 +1,4 @@ +meta { + name: cash-wallet-cutover + seq: 10 +} diff --git a/dev/bruno/Flash GraphQL API/notoken/queries/cash-wallet-cutover.bru b/dev/bruno/Flash GraphQL API/notoken/queries/cash-wallet-cutover.bru new file mode 100644 index 000000000..1c6f7cd06 --- /dev/null +++ b/dev/bruno/Flash GraphQL API/notoken/queries/cash-wallet-cutover.bru @@ -0,0 +1,44 @@ +meta { + name: cash-wallet-cutover + type: graphql + seq: 11 +} + +post { + url: {{flashGraphqlUrl}} + body: graphql + auth: inherit +} + +body:graphql { + query CashWalletCutoverPublicState { + cashWalletCutover { + state + scheduledAt + startedAt + completedAt + pausedAt + pauseReason + cutoverVersion + runId + updatedBy + updatedAt + } + } +} + +body:graphql:vars { + {} +} + +script:post-response { + test("public cutover state returns without GraphQL errors", function () { + const jsonData = res.getBody() + expect(jsonData.errors).to.be.undefined + expect(jsonData.data.cashWalletCutover.state).to.be.oneOf([ + "PRE", + "IN_PROGRESS", + "COMPLETE", + ]) + }) +} diff --git a/operator-runs/eng-345-manual-347/findings.md b/operator-runs/eng-345-manual-347/findings.md new file mode 100644 index 000000000..79191ab38 --- /dev/null +++ b/operator-runs/eng-345-manual-347/findings.md @@ -0,0 +1,6 @@ +# Findings + +- `preparePrimaryCashWalletCutover` accepts injected repositories, so the test can scope discovery to the 10 created accounts by passing an `accountsRepo.listUnlockedAccounts()` generator for only those IDs. +- Account creation creates both USD and USDT checking wallets and defaults new accounts to USDT. For this test, each new account must be explicitly updated back to the USD wallet with `Accounts.updateDefaultWalletId`. +- Funding can use `Payments.intraledgerPaymentSendWalletIdForUsdWallet` from the funder account's USD wallet to each target legacy USD wallet. The amount argument is cents, so `$0.25` is `25` and `$0.01` is `1`. +- Cutover batch execution can use `CashWalletCutover.runPrimaryCashWalletCutoverBatch`; status and lifecycle can use the existing app lifecycle functions. diff --git a/operator-runs/eng-345-manual-347/funding-retry-results.json b/operator-runs/eng-345-manual-347/funding-retry-results.json new file mode 100644 index 000000000..0f9591d76 --- /dev/null +++ b/operator-runs/eng-345-manual-347/funding-retry-results.json @@ -0,0 +1,97 @@ +{ + "targetCents": 25, + "funderUsdWalletId": "37a3bce4-1930-484d-bbfc-279c1f8bfb66", + "results": [ + { + "index": 1, + "accountId": "6a11ada7e55310755eeb0257", + "legacyUsdWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "beforeCents": 0, + "targetCents": 25, + "deltaCents": 25, + "status": { + "value": "success" + }, + "error": null, + "afterCents": 25 + }, + { + "index": 2, + "accountId": "6a11ada8e55310755eeb0271", + "legacyUsdWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "beforeCents": 0, + "targetCents": 25, + "deltaCents": 25, + "status": { + "value": "success" + }, + "error": null, + "afterCents": 25 + }, + { + "index": 3, + "accountId": "6a11ada9e55310755eeb028b", + "legacyUsdWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "beforeCents": 0, + "targetCents": 25, + "deltaCents": 25, + "status": { + "value": "success" + }, + "error": null, + "afterCents": 25 + }, + { + "index": 4, + "accountId": "6a11ada9e55310755eeb02a5", + "legacyUsdWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "beforeCents": 0, + "targetCents": 25, + "deltaCents": 25, + "status": { + "value": "success" + }, + "error": null, + "afterCents": 25 + }, + { + "index": 5, + "accountId": "6a11adabe55310755eeb02bf", + "legacyUsdWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "beforeCents": 0, + "targetCents": 25, + "deltaCents": 25, + "status": { + "value": "success" + }, + "error": null, + "afterCents": 25 + }, + { + "index": 6, + "accountId": "6a11adace55310755eeb02d9", + "legacyUsdWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "beforeCents": 0, + "targetCents": 25, + "deltaCents": 25, + "status": { + "value": "success" + }, + "error": null, + "afterCents": 25 + }, + { + "index": 7, + "accountId": "6a11adade55310755eeb02f3", + "legacyUsdWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "beforeCents": 0, + "targetCents": 25, + "deltaCents": 25, + "status": { + "value": "success" + }, + "error": null, + "afterCents": 25 + } + ] +} diff --git a/operator-runs/eng-345-manual-347/manifest.json b/operator-runs/eng-345-manual-347/manifest.json new file mode 100644 index 000000000..e1af02bce --- /dev/null +++ b/operator-runs/eng-345-manual-347/manifest.json @@ -0,0 +1,78 @@ +{ + "cutoverVersion": 347, + "runId": "manual-eng-347", + "createdAt": "2026-05-23T13:37:44.318Z", + "funderUsdWalletId": "37a3bce4-1930-484d-bbfc-279c1f8bfb66", + "accounts": [ + { + "index": 1, + "phone": "+16509-recovered-01", + "kratosUserId": "03cc58c6-a325-4966-9cf0-a96a58a8b366", + "accountId": "6a11ada7e55310755eeb0257", + "accountUuid": "2da55813-e2a6-4bfc-946d-dd52af5b6a99", + "legacyUsdWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "destinationUsdtWalletId": "8a97ec4c-bf67-4a74-a5c2-bc8cf28e99c6", + "startingFundingCents": 25 + }, + { + "index": 2, + "phone": "+16509-recovered-02", + "kratosUserId": "4b7a6cea-7024-47d2-a4ba-f9df7d97aa7b", + "accountId": "6a11ada8e55310755eeb0271", + "accountUuid": "71b5a288-4a46-4e9d-858c-87012d55c12e", + "legacyUsdWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "destinationUsdtWalletId": "741a97b0-9612-4f34-af32-ed28c0a3b5fd", + "startingFundingCents": 25 + }, + { + "index": 3, + "phone": "+16509-recovered-03", + "kratosUserId": "cb7eee02-7cb5-41a3-a4a5-1210624cb309", + "accountId": "6a11ada9e55310755eeb028b", + "accountUuid": "a822f6c2-c8ed-4c6e-a5ce-20c93abaad66", + "legacyUsdWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "destinationUsdtWalletId": "144db453-fdb8-4180-b405-097104637644", + "startingFundingCents": 25 + }, + { + "index": 4, + "phone": "+16509-recovered-04", + "kratosUserId": "76fe0bc7-1a9c-440b-9586-317781c153f4", + "accountId": "6a11ada9e55310755eeb02a5", + "accountUuid": "87c4db3b-0aa8-47e3-942d-8fe845a6c6b7", + "legacyUsdWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "destinationUsdtWalletId": "ae3bddb2-1bb7-4019-8390-39694881c91b", + "startingFundingCents": 25 + }, + { + "index": 5, + "phone": "+16509-recovered-05", + "kratosUserId": "4736f580-cd98-4750-ab16-1da95e986dc9", + "accountId": "6a11adabe55310755eeb02bf", + "accountUuid": "bbf1af43-70ef-4e22-a40c-eba31289735a", + "legacyUsdWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "destinationUsdtWalletId": "fa7c7780-9246-4088-beb6-56fcd98e3209", + "startingFundingCents": 25 + }, + { + "index": 6, + "phone": "+16509-recovered-06", + "kratosUserId": "c5cab6ac-a56e-431b-a460-118d3627f7d3", + "accountId": "6a11adace55310755eeb02d9", + "accountUuid": "06ee59a8-8d59-4139-99d1-92378e5b8011", + "legacyUsdWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "destinationUsdtWalletId": "53884f7a-0a55-4406-8b60-3c6d68a6c180", + "startingFundingCents": 25 + }, + { + "index": 7, + "phone": "+16509-recovered-07", + "kratosUserId": "c1df1aa1-18c6-4ec9-97a6-9dfc5736be7f", + "accountId": "6a11adade55310755eeb02f3", + "accountUuid": "019555c0-b26a-468f-b11e-5bbe69048e02", + "legacyUsdWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "destinationUsdtWalletId": "80f0de1f-b4e6-4904-8c3f-d90bad763ce2", + "startingFundingCents": 25 + } + ] +} diff --git a/operator-runs/eng-345-manual-347/prep-348-results.json b/operator-runs/eng-345-manual-347/prep-348-results.json new file mode 100644 index 000000000..951b9cb85 --- /dev/null +++ b/operator-runs/eng-345-manual-347/prep-348-results.json @@ -0,0 +1,344 @@ +{ + "cutoverVersion": 348, + "runId": "manual-eng-348", + "flips": [ + { + "index": 1, + "accountId": "6a11ada7e55310755eeb0257", + "defaultWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25" + }, + { + "index": 2, + "accountId": "6a11ada8e55310755eeb0271", + "defaultWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25" + }, + { + "index": 3, + "accountId": "6a11ada9e55310755eeb028b", + "defaultWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25" + }, + { + "index": 4, + "accountId": "6a11ada9e55310755eeb02a5", + "defaultWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25" + }, + { + "index": 5, + "accountId": "6a11adabe55310755eeb02bf", + "defaultWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25" + }, + { + "index": 6, + "accountId": "6a11adace55310755eeb02d9", + "defaultWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25" + }, + { + "index": 7, + "accountId": "6a11adade55310755eeb02f3", + "defaultWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25" + } + ], + "configReset": { + "acknowledged": true, + "modifiedCount": 1, + "upsertedId": null, + "upsertedCount": 0, + "matchedCount": 1 + }, + "preview": { + "report": { + "cutoverVersion": 348, + "runId": "manual-eng-348", + "totalAccounts": 7, + "migrationCandidates": 7, + "alreadyUsdt": 0, + "residualLegacyUsd": 0, + "blockers": 0, + "blockerAccounts": [], + "canStart": true + }, + "plannedMigrations": [ + { + "accountId": "6a11ada7e55310755eeb0257", + "accountUuid": "2da55813-e2a6-4bfc-946d-dd52af5b6a99", + "legacyUsdWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "destinationUsdtWalletId": "8a97ec4c-bf67-4a74-a5c2-bc8cf28e99c6", + "previousDefaultWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada7e55310755eeb0257" + }, + { + "accountId": "6a11ada8e55310755eeb0271", + "accountUuid": "71b5a288-4a46-4e9d-858c-87012d55c12e", + "legacyUsdWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "destinationUsdtWalletId": "741a97b0-9612-4f34-af32-ed28c0a3b5fd", + "previousDefaultWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada8e55310755eeb0271" + }, + { + "accountId": "6a11ada9e55310755eeb028b", + "accountUuid": "a822f6c2-c8ed-4c6e-a5ce-20c93abaad66", + "legacyUsdWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "destinationUsdtWalletId": "144db453-fdb8-4180-b405-097104637644", + "previousDefaultWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada9e55310755eeb028b" + }, + { + "accountId": "6a11ada9e55310755eeb02a5", + "accountUuid": "87c4db3b-0aa8-47e3-942d-8fe845a6c6b7", + "legacyUsdWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "destinationUsdtWalletId": "ae3bddb2-1bb7-4019-8390-39694881c91b", + "previousDefaultWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada9e55310755eeb02a5" + }, + { + "accountId": "6a11adabe55310755eeb02bf", + "accountUuid": "bbf1af43-70ef-4e22-a40c-eba31289735a", + "legacyUsdWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "destinationUsdtWalletId": "fa7c7780-9246-4088-beb6-56fcd98e3209", + "previousDefaultWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adabe55310755eeb02bf" + }, + { + "accountId": "6a11adace55310755eeb02d9", + "accountUuid": "06ee59a8-8d59-4139-99d1-92378e5b8011", + "legacyUsdWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "destinationUsdtWalletId": "53884f7a-0a55-4406-8b60-3c6d68a6c180", + "previousDefaultWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adace55310755eeb02d9" + }, + { + "accountId": "6a11adade55310755eeb02f3", + "accountUuid": "019555c0-b26a-468f-b11e-5bbe69048e02", + "legacyUsdWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "destinationUsdtWalletId": "80f0de1f-b4e6-4904-8c3f-d90bad763ce2", + "previousDefaultWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adade55310755eeb02f3" + } + ] + }, + "prepared": { + "report": { + "cutoverVersion": 348, + "runId": "manual-eng-348", + "totalAccounts": 7, + "migrationCandidates": 7, + "alreadyUsdt": 0, + "residualLegacyUsd": 0, + "blockers": 0, + "blockerAccounts": [], + "canStart": true + }, + "plannedMigrations": [ + { + "accountId": "6a11ada7e55310755eeb0257", + "accountUuid": "2da55813-e2a6-4bfc-946d-dd52af5b6a99", + "legacyUsdWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "destinationUsdtWalletId": "8a97ec4c-bf67-4a74-a5c2-bc8cf28e99c6", + "previousDefaultWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada7e55310755eeb0257" + }, + { + "accountId": "6a11ada8e55310755eeb0271", + "accountUuid": "71b5a288-4a46-4e9d-858c-87012d55c12e", + "legacyUsdWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "destinationUsdtWalletId": "741a97b0-9612-4f34-af32-ed28c0a3b5fd", + "previousDefaultWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada8e55310755eeb0271" + }, + { + "accountId": "6a11ada9e55310755eeb028b", + "accountUuid": "a822f6c2-c8ed-4c6e-a5ce-20c93abaad66", + "legacyUsdWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "destinationUsdtWalletId": "144db453-fdb8-4180-b405-097104637644", + "previousDefaultWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada9e55310755eeb028b" + }, + { + "accountId": "6a11ada9e55310755eeb02a5", + "accountUuid": "87c4db3b-0aa8-47e3-942d-8fe845a6c6b7", + "legacyUsdWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "destinationUsdtWalletId": "ae3bddb2-1bb7-4019-8390-39694881c91b", + "previousDefaultWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada9e55310755eeb02a5" + }, + { + "accountId": "6a11adabe55310755eeb02bf", + "accountUuid": "bbf1af43-70ef-4e22-a40c-eba31289735a", + "legacyUsdWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "destinationUsdtWalletId": "fa7c7780-9246-4088-beb6-56fcd98e3209", + "previousDefaultWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adabe55310755eeb02bf" + }, + { + "accountId": "6a11adace55310755eeb02d9", + "accountUuid": "06ee59a8-8d59-4139-99d1-92378e5b8011", + "legacyUsdWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "destinationUsdtWalletId": "53884f7a-0a55-4406-8b60-3c6d68a6c180", + "previousDefaultWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adace55310755eeb02d9" + }, + { + "accountId": "6a11adade55310755eeb02f3", + "accountUuid": "019555c0-b26a-468f-b11e-5bbe69048e02", + "legacyUsdWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "destinationUsdtWalletId": "80f0de1f-b4e6-4904-8c3f-d90bad763ce2", + "previousDefaultWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adade55310755eeb02f3" + } + ], + "migrations": [ + { + "id": "24353ef3-66ba-4acc-9365-1ad6e83301f0", + "accountId": "6a11ada7e55310755eeb0257", + "accountUuid": "2da55813-e2a6-4bfc-946d-dd52af5b6a99", + "legacyUsdWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "destinationUsdtWalletId": "8a97ec4c-bf67-4a74-a5c2-bc8cf28e99c6", + "previousDefaultWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "status": "not_started", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada7e55310755eeb0257", + "attempts": 0, + "updatedAt": "2026-05-23T14:49:36.370Z" + }, + { + "id": "d47e3b77-39d0-49ea-a5eb-e0dbbf5a306c", + "accountId": "6a11ada8e55310755eeb0271", + "accountUuid": "71b5a288-4a46-4e9d-858c-87012d55c12e", + "legacyUsdWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "destinationUsdtWalletId": "741a97b0-9612-4f34-af32-ed28c0a3b5fd", + "previousDefaultWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "status": "not_started", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada8e55310755eeb0271", + "attempts": 0, + "updatedAt": "2026-05-23T14:49:36.382Z" + }, + { + "id": "b5b42c93-519a-4103-b7fb-a1ff5a3dd17d", + "accountId": "6a11ada9e55310755eeb028b", + "accountUuid": "a822f6c2-c8ed-4c6e-a5ce-20c93abaad66", + "legacyUsdWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "destinationUsdtWalletId": "144db453-fdb8-4180-b405-097104637644", + "previousDefaultWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "status": "not_started", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada9e55310755eeb028b", + "attempts": 0, + "updatedAt": "2026-05-23T14:49:36.393Z" + }, + { + "id": "1eafc20f-3e53-4d72-ba25-48cd8d5c5c24", + "accountId": "6a11ada9e55310755eeb02a5", + "accountUuid": "87c4db3b-0aa8-47e3-942d-8fe845a6c6b7", + "legacyUsdWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "destinationUsdtWalletId": "ae3bddb2-1bb7-4019-8390-39694881c91b", + "previousDefaultWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "status": "not_started", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11ada9e55310755eeb02a5", + "attempts": 0, + "updatedAt": "2026-05-23T14:49:36.401Z" + }, + { + "id": "78fdd4b6-9c81-4883-a0e0-0bab20491c17", + "accountId": "6a11adabe55310755eeb02bf", + "accountUuid": "bbf1af43-70ef-4e22-a40c-eba31289735a", + "legacyUsdWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "destinationUsdtWalletId": "fa7c7780-9246-4088-beb6-56fcd98e3209", + "previousDefaultWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "status": "not_started", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adabe55310755eeb02bf", + "attempts": 0, + "updatedAt": "2026-05-23T14:49:36.406Z" + }, + { + "id": "9cf2b55b-d118-4b2a-a322-9c857e2dee8d", + "accountId": "6a11adace55310755eeb02d9", + "accountUuid": "06ee59a8-8d59-4139-99d1-92378e5b8011", + "legacyUsdWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "destinationUsdtWalletId": "53884f7a-0a55-4406-8b60-3c6d68a6c180", + "previousDefaultWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "status": "not_started", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adace55310755eeb02d9", + "attempts": 0, + "updatedAt": "2026-05-23T14:49:36.414Z" + }, + { + "id": "20e0769a-6c20-4475-bb0a-9eda26698668", + "accountId": "6a11adade55310755eeb02f3", + "accountUuid": "019555c0-b26a-468f-b11e-5bbe69048e02", + "legacyUsdWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "destinationUsdtWalletId": "80f0de1f-b4e6-4904-8c3f-d90bad763ce2", + "previousDefaultWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "status": "not_started", + "idempotencyKey": "cash-wallet-cutover:manual-eng-348:6a11adade55310755eeb02f3", + "attempts": 0, + "updatedAt": "2026-05-23T14:49:36.424Z" + } + ] + }, + "status": { + "config": { + "state": "pre", + "updatedBy": "manual-local", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "updatedAt": "2026-05-23T14:49:36.246Z" + }, + "countsByStatus": { + "not_started": 7 + } + } +} diff --git a/operator-runs/eng-345-manual-347/progress.md b/operator-runs/eng-345-manual-347/progress.md new file mode 100644 index 000000000..2455d031d --- /dev/null +++ b/operator-runs/eng-345-manual-347/progress.md @@ -0,0 +1,45 @@ +- 2026-05-23T13:37:13.813Z ERROR Cannot read properties of undefined (reading 'findOne') +- 2026-05-23T13:37:44.318Z created account 1: 6a11ada7e55310755eeb0257 USD=0a4d1c55-d8ec-4685-8457-216d41569d61 USDT=8a97ec4c-bf67-4a74-a5c2-bc8cf28e99c6 funding=25 +- 2026-05-23T13:37:45.186Z created account 2: 6a11ada8e55310755eeb0271 USD=08ee5f13-b4c3-4ff5-835a-55dd786d3887 USDT=741a97b0-9612-4f34-af32-ed28c0a3b5fd funding=25 +- 2026-05-23T13:37:45.866Z created account 3: 6a11ada9e55310755eeb028b USD=697b8583-6e1d-47d5-aba0-8b2c8aa6bc32 USDT=144db453-fdb8-4180-b405-097104637644 funding=25 +- 2026-05-23T13:37:47.835Z created account 4: 6a11ada9e55310755eeb02a5 USD=dbdfba0c-ead0-4c94-96f8-e15130b7f796 USDT=ae3bddb2-1bb7-4019-8390-39694881c91b funding=25 +- 2026-05-23T13:37:48.713Z created account 5: 6a11adabe55310755eeb02bf USD=9756a443-d30d-4b1c-a797-933ec2ced1d0 USDT=fa7c7780-9246-4088-beb6-56fcd98e3209 funding=25 +- 2026-05-23T13:37:49.430Z created account 6: 6a11adace55310755eeb02d9 USD=ecaf103c-d024-419a-8bad-b0c61e8068be USDT=53884f7a-0a55-4406-8b60-3c6d68a6c180 funding=25 +- 2026-05-23T13:37:50.263Z created account 7: 6a11adade55310755eeb02f3 USD=8a3e1bd4-4d2d-4f97-b201-d58d3723a53c USDT=80f0de1f-b4e6-4904-8c3f-d90bad763ce2 funding=25 +- 2026-05-23T13:37:50.789Z ERROR +- 2026-05-23T13:39:59.481Z ERROR +- 2026-05-23T13:41:36.710Z ERROR +- 2026-05-23T13:42:04.155Z wrote verification results to /Users/dread/Documents/Island-Bitcoin/Flash/flash/.worktrees/eng-345-review/operator-runs/eng-345-manual-347/results.json +- 2026-05-23T13:42:26.832Z ERROR +- 2026-05-23T13:44:28.884Z ERROR +- 2026-05-23T13:50:13.799Z preview planned=7 +- 2026-05-23T13:50:27.921Z prepared migrations=7 +- 2026-05-23T13:50:44.208Z reset singleton cutover config to pre for manual-eng-347 +- 2026-05-23T13:50:48.388Z started state=in_progress +- 2026-05-23T13:51:03.235Z batch 1: {"attempted":7,"advanced":7,"failed":0,"skipped":0} +- 2026-05-23T13:51:03.298Z batch 2: {"attempted":7,"advanced":7,"failed":0,"skipped":0} +- 2026-05-23T13:51:06.709Z batch 3: {"attempted":7,"advanced":7,"failed":0,"skipped":0} +- 2026-05-23T13:51:06.840Z batch 4: {"attempted":7,"advanced":7,"failed":0,"skipped":0} +- 2026-05-23T13:51:08.144Z batch 5: {"attempted":7,"advanced":7,"failed":0,"skipped":0} +- 2026-05-23T13:51:08.238Z batch 6: {"attempted":7,"advanced":7,"failed":0,"skipped":0} +- 2026-05-23T13:51:08.268Z all migrations complete after batch 6 +- 2026-05-23T13:51:25.286Z completed lifecycle state=complete +- 2026-05-23T13:51:31.895Z wrote verification results to /Users/dread/Documents/Island-Bitcoin/Flash/flash/.worktrees/eng-345-review/operator-runs/eng-345-manual-347/results.json +- 2026-05-23T14:05:39.454Z funding retry account 1: before=0 delta=25 status=[object Object] after=25 +- 2026-05-23T14:05:42.219Z funding retry account 2: before=0 delta=25 status=[object Object] after=25 +- 2026-05-23T14:05:44.498Z funding retry account 3: before=0 delta=25 status=[object Object] after=25 +- 2026-05-23T14:05:46.786Z funding retry account 4: before=0 delta=25 status=[object Object] after=25 +- 2026-05-23T14:05:49.479Z funding retry account 5: before=0 delta=25 status=[object Object] after=25 +- 2026-05-23T14:05:50.986Z funding retry account 6: before=0 delta=25 status=[object Object] after=25 +- 2026-05-23T14:05:52.277Z funding retry account 7: before=0 delta=25 status=[object Object] after=25 +- 2026-05-23T14:06:31.393Z wrote verification results to /Users/dread/Documents/Island-Bitcoin/Flash/flash/.worktrees/eng-345-review/operator-runs/eng-345-manual-347/results.json +- 2026-05-23T14:49:35.240Z reset account 1 defaultWalletId=0a4d1c55-d8ec-4685-8457-216d41569d61 fundedUsdCents=25 +- 2026-05-23T14:49:35.433Z reset account 2 defaultWalletId=08ee5f13-b4c3-4ff5-835a-55dd786d3887 fundedUsdCents=25 +- 2026-05-23T14:49:35.582Z reset account 3 defaultWalletId=697b8583-6e1d-47d5-aba0-8b2c8aa6bc32 fundedUsdCents=25 +- 2026-05-23T14:49:35.729Z reset account 4 defaultWalletId=dbdfba0c-ead0-4c94-96f8-e15130b7f796 fundedUsdCents=25 +- 2026-05-23T14:49:35.950Z reset account 5 defaultWalletId=9756a443-d30d-4b1c-a797-933ec2ced1d0 fundedUsdCents=25 +- 2026-05-23T14:49:36.092Z reset account 6 defaultWalletId=ecaf103c-d024-419a-8bad-b0c61e8068be fundedUsdCents=25 +- 2026-05-23T14:49:36.245Z reset account 7 defaultWalletId=8a3e1bd4-4d2d-4f97-b201-d58d3723a53c fundedUsdCents=25 +- 2026-05-23T14:49:36.256Z reset singleton cutover config to pre for manual-eng-348 +- 2026-05-23T14:49:36.323Z preview manual-eng-348 planned=7 +- 2026-05-23T14:49:36.431Z prepared manual-eng-348 migrations=7 diff --git a/operator-runs/eng-345-manual-347/results.json b/operator-runs/eng-345-manual-347/results.json new file mode 100644 index 000000000..57d76e89c --- /dev/null +++ b/operator-runs/eng-345-manual-347/results.json @@ -0,0 +1,95 @@ +{ + "status": { + "config": { + "state": "complete", + "startedAt": "2026-05-23T13:50:48.355Z", + "completedAt": "2026-05-23T13:51:25.254Z", + "updatedBy": "manual-local", + "cutoverVersion": 347, + "runId": "manual-eng-347", + "updatedAt": "2026-05-23T13:51:25.281Z" + }, + "countsByStatus": { + "complete": 7 + } + }, + "accounts": [ + { + "index": 1, + "accountId": "6a11ada7e55310755eeb0257", + "expectedFundingCents": 25, + "defaultWalletId": "8a97ec4c-bf67-4a74-a5c2-bc8cf28e99c6", + "defaultIsDestinationUsdt": true, + "legacyUsdWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "destinationUsdtWalletId": "8a97ec4c-bf67-4a74-a5c2-bc8cf28e99c6", + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 2, + "accountId": "6a11ada8e55310755eeb0271", + "expectedFundingCents": 25, + "defaultWalletId": "741a97b0-9612-4f34-af32-ed28c0a3b5fd", + "defaultIsDestinationUsdt": true, + "legacyUsdWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "destinationUsdtWalletId": "741a97b0-9612-4f34-af32-ed28c0a3b5fd", + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 3, + "accountId": "6a11ada9e55310755eeb028b", + "expectedFundingCents": 25, + "defaultWalletId": "144db453-fdb8-4180-b405-097104637644", + "defaultIsDestinationUsdt": true, + "legacyUsdWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "destinationUsdtWalletId": "144db453-fdb8-4180-b405-097104637644", + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 4, + "accountId": "6a11ada9e55310755eeb02a5", + "expectedFundingCents": 25, + "defaultWalletId": "ae3bddb2-1bb7-4019-8390-39694881c91b", + "defaultIsDestinationUsdt": true, + "legacyUsdWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "destinationUsdtWalletId": "ae3bddb2-1bb7-4019-8390-39694881c91b", + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 5, + "accountId": "6a11adabe55310755eeb02bf", + "expectedFundingCents": 25, + "defaultWalletId": "fa7c7780-9246-4088-beb6-56fcd98e3209", + "defaultIsDestinationUsdt": true, + "legacyUsdWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "destinationUsdtWalletId": "fa7c7780-9246-4088-beb6-56fcd98e3209", + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 6, + "accountId": "6a11adace55310755eeb02d9", + "expectedFundingCents": 25, + "defaultWalletId": "53884f7a-0a55-4406-8b60-3c6d68a6c180", + "defaultIsDestinationUsdt": true, + "legacyUsdWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "destinationUsdtWalletId": "53884f7a-0a55-4406-8b60-3c6d68a6c180", + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 7, + "accountId": "6a11adade55310755eeb02f3", + "expectedFundingCents": 25, + "defaultWalletId": "80f0de1f-b4e6-4904-8c3f-d90bad763ce2", + "defaultIsDestinationUsdt": true, + "legacyUsdWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "destinationUsdtWalletId": "80f0de1f-b4e6-4904-8c3f-d90bad763ce2", + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + } + ] +} diff --git a/operator-runs/eng-345-manual-347/run.ts b/operator-runs/eng-345-manual-347/run.ts new file mode 100644 index 000000000..3438416d3 --- /dev/null +++ b/operator-runs/eng-345-manual-347/run.ts @@ -0,0 +1,477 @@ +import fs from "fs/promises" +import path from "path" +import { randomUUID } from "crypto" + +import { getDefaultAccountsConfig } from "@config" + +import { Accounts, CashWalletCutover, Payments, Wallets } from "@app" +import { getBalanceForWallet } from "@app/wallets" +import { WalletCurrency } from "@domain/shared" +import { PaymentSendStatus } from "@domain/bitcoin/lightning" +import { WalletType } from "@domain/wallets" + +import { setupMongoConnection } from "@services/mongodb" +import { + AccountsRepository, + CashWalletCutoverRepository, + WalletsRepository, +} from "@services/mongoose" +import { Account, CashWalletCutoverConfig } from "@services/mongoose/schema" + +type TargetAccount = { + index: number + phone: string + kratosUserId: string + accountId: string + accountUuid?: string + legacyUsdWalletId: string + destinationUsdtWalletId: string + startingFundingCents: number +} + +type Manifest = { + cutoverVersion: number + runId: string + createdAt: string + funderUsdWalletId?: string + accounts: TargetAccount[] +} + +const CUTOVER_VERSION = 347 +const RUN_ID = "manual-eng-347" +const ACCOUNT_COUNT = 10 +const OUTPUT_DIR = path.resolve("operator-runs/eng-345-manual-347") +const MANIFEST_PATH = path.join(OUTPUT_DIR, "manifest.json") +const RESULTS_PATH = path.join(OUTPUT_DIR, "results.json") +const PROGRESS_PATH = path.join(OUTPUT_DIR, "progress.md") + +const logProgress = async (line: string) => { + await fs.appendFile(PROGRESS_PATH, `- ${new Date().toISOString()} ${line}\n`) +} + +const throwIfError = (result: T | Error): T => { + if (result instanceof Error) throw result + return result +} + +const loadManifest = async (): Promise => { + const raw = await fs.readFile(MANIFEST_PATH, "utf8") + return JSON.parse(raw) as Manifest +} + +const saveManifest = async (manifest: Manifest) => { + await fs.writeFile(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`) +} + +const listWalletsForAccount = async (accountId: string) => { + const wallets = throwIfError( + await WalletsRepository().listByAccountId(accountId as AccountId), + ) + const usd = wallets.find((wallet) => wallet.currency === WalletCurrency.Usd) + const usdt = wallets.find((wallet) => wallet.currency === WalletCurrency.Usdt) + + if (!usd) throw new Error(`Missing USD wallet for account ${accountId}`) + if (!usdt) throw new Error(`Missing USDT wallet for account ${accountId}`) + + return { usd, usdt } +} + +const funderUsdWalletId = async (): Promise => { + const funder = await Account.findOne({ role: "funder" }) + if (!funder) throw new Error("Missing funder account") + + const wallets = throwIfError( + await WalletsRepository().listByAccountId(funder._id.toString() as AccountId), + ) + const usdWallet = wallets.find((wallet) => wallet.currency === WalletCurrency.Usd) + if (!usdWallet) throw new Error("Missing funder USD wallet") + + return usdWallet.id +} + +const createAccounts = async () => { + await fs.mkdir(OUTPUT_DIR, { recursive: true }) + + const config = getDefaultAccountsConfig() + const existing = await fs + .readFile(MANIFEST_PATH, "utf8") + .then((raw) => JSON.parse(raw) as Manifest) + .catch(() => undefined) + const funderWalletId = existing?.funderUsdWalletId ?? (await funderUsdWalletId()) + const accounts: TargetAccount[] = existing?.accounts ?? [] + const stamp = Date.now().toString().slice(-7) + + for (let i = accounts.length + 1; i <= ACCOUNT_COUNT; i += 1) { + const suffix = `${stamp}${String(i).padStart(2, "0")}` + const phone = `+16509${suffix}` as PhoneNumber + const kratosUserId = randomUUID() as UserId + + const account = throwIfError( + await Accounts.createAccountWithPhoneIdentifier({ + newAccountInfo: { kratosUserId, phone }, + config, + }), + ) + + const { usd, usdt } = await listWalletsForAccount(account.id) + + throwIfError( + await Accounts.updateDefaultWalletId({ + accountId: account.id, + walletId: usd.id, + }), + ) + + const fundingCents = i <= 8 ? 25 : i === 9 ? 1 : 0 + + accounts.push({ + index: i, + phone, + kratosUserId, + accountId: account.id, + accountUuid: account.uuid, + legacyUsdWalletId: usd.id, + destinationUsdtWalletId: usdt.id, + startingFundingCents: fundingCents, + }) + + await logProgress( + `created account ${i}: ${account.id} USD=${usd.id} USDT=${usdt.id} funding=${fundingCents}`, + ) + + await saveManifest({ + cutoverVersion: CUTOVER_VERSION, + runId: RUN_ID, + createdAt: existing?.createdAt ?? new Date().toISOString(), + funderUsdWalletId: funderWalletId, + accounts, + }) + } + + const manifest: Manifest = { + cutoverVersion: CUTOVER_VERSION, + runId: RUN_ID, + createdAt: existing?.createdAt ?? new Date().toISOString(), + funderUsdWalletId: funderWalletId, + accounts, + } + await saveManifest(manifest) + console.log(JSON.stringify(manifest, null, 2)) +} + +const completePartialAccounts = async () => { + const manifest = await loadManifest() + const existingIds = new Set(manifest.accounts.map((account) => account.accountId)) + const partials = await Account.find({ + role: "user", + defaultWalletId: { $exists: false }, + created_at: { $gte: new Date(Date.now() - 60 * 60 * 1000) }, + }).sort({ created_at: 1 }) + + for (const partial of partials) { + if (manifest.accounts.length >= ACCOUNT_COUNT) break + const accountId = partial._id.toString() + if (existingIds.has(accountId)) continue + + const usd = throwIfError( + await WalletsRepository().persistNew({ + accountId: accountId as AccountId, + type: WalletType.Checking, + currency: WalletCurrency.Usd, + }), + ) + const usdt = throwIfError( + await WalletsRepository().persistNew({ + accountId: accountId as AccountId, + type: WalletType.Checking, + currency: WalletCurrency.Usdt, + }), + ) + throwIfError( + await Accounts.updateDefaultWalletId({ + accountId: accountId as AccountId, + walletId: usd.id, + }), + ) + + const index = manifest.accounts.length + 1 + const fundingCents = index <= 8 ? 25 : index === 9 ? 1 : 0 + manifest.accounts.push({ + index, + phone: `partial-${index}`, + kratosUserId: partial.kratosUserId, + accountId, + accountUuid: partial.id, + legacyUsdWalletId: usd.id, + destinationUsdtWalletId: usdt.id, + startingFundingCents: fundingCents, + }) + existingIds.add(accountId) + await saveManifest(manifest) + await logProgress( + `completed partial account ${index}: ${accountId} USD=${usd.id} USDT=${usdt.id} funding=${fundingCents}`, + ) + } + + console.log(JSON.stringify(manifest, null, 2)) +} + +const fundAccounts = async () => { + const manifest = await loadManifest() + if (!manifest.funderUsdWalletId) { + manifest.funderUsdWalletId = await funderUsdWalletId() + await saveManifest(manifest) + } + + for (const account of manifest.accounts) { + if (account.startingFundingCents === 0) { + await logProgress(`left account ${account.index} unfunded`) + continue + } + + const status = throwIfError( + await Payments.intraledgerPaymentSendWalletIdForUsdWallet({ + senderWalletId: manifest.funderUsdWalletId, + recipientWalletId: account.legacyUsdWalletId, + amount: account.startingFundingCents, + memo: `ENG-345 ${RUN_ID} seed ${account.index}`, + }), + ) + + if (status !== PaymentSendStatus.Success && status !== PaymentSendStatus.Pending) { + throw new Error(`Funding account ${account.index} returned ${status}`) + } + + await logProgress( + `funded account ${account.index} ${account.legacyUsdWalletId} with ${account.startingFundingCents} cents status=${status}`, + ) + } +} + +const scopedAccountsRepo = (targetIds: Set) => ({ + async *listUnlockedAccounts() { + for (const accountId of targetIds) { + yield throwIfError(await AccountsRepository().findById(accountId as AccountId)) + } + }, +}) + +const preview = async () => { + const manifest = await loadManifest() + const result = throwIfError( + await CashWalletCutover.previewPrimaryCashWalletCutover({ + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + accountsRepo: scopedAccountsRepo(new Set(manifest.accounts.map((a) => a.accountId))), + walletsRepo: WalletsRepository(), + }), + ) + console.log(JSON.stringify(result, null, 2)) + await logProgress(`preview planned=${result.plannedMigrations.length}`) +} + +const prepare = async () => { + const manifest = await loadManifest() + const result = throwIfError( + await CashWalletCutover.preparePrimaryCashWalletCutover({ + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + accountsRepo: scopedAccountsRepo(new Set(manifest.accounts.map((a) => a.accountId))), + walletsRepo: WalletsRepository(), + migrationsRepo: CashWalletCutoverRepository(), + }), + ) + console.log(JSON.stringify(result, null, 2)) + await logProgress(`prepared migrations=${result.migrations.length}`) +} + +const start = async () => { + const manifest = await loadManifest() + const result = throwIfError( + await CashWalletCutover.startPrimaryCashWalletCutover({ + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + actor: "manual-local", + migrationsRepo: CashWalletCutoverRepository(), + }), + ) + console.log(JSON.stringify(result, null, 2)) + await logProgress(`started state=${result.state}`) +} + +const runBatches = async () => { + const manifest = await loadManifest() + const batches = [] + + for (let i = 1; i <= 40; i += 1) { + const result = throwIfError( + await CashWalletCutover.runPrimaryCashWalletCutoverBatch({ + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + workerId: "manual-local", + limit: ACCOUNT_COUNT, + lockStaleBefore: new Date(Date.now() - 300_000), + migrationsRepo: CashWalletCutoverRepository(), + }), + ) + batches.push(result) + await logProgress(`batch ${i}: ${JSON.stringify(result)}`) + + const status = throwIfError( + await CashWalletCutover.getPrimaryCashWalletCutoverStatus({ + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + migrationsRepo: CashWalletCutoverRepository(), + }), + ) + + if (result.failed > 0) { + console.log(JSON.stringify({ batches, status }, null, 2)) + throw new Error(`Batch ${i} failed`) + } + + if (Object.keys(status.countsByStatus).length === 1 && status.countsByStatus.complete) { + console.log(JSON.stringify({ batches, status }, null, 2)) + await logProgress(`all migrations complete after batch ${i}`) + return + } + + if (result.attempted === 0) { + console.log(JSON.stringify({ batches, status }, null, 2)) + throw new Error("No runnable migrations, but run is not complete") + } + } + + throw new Error("Exceeded maximum batch count") +} + +const complete = async () => { + const manifest = await loadManifest() + const result = throwIfError( + await CashWalletCutover.completePrimaryCashWalletCutover({ + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + actor: "manual-local", + migrationsRepo: CashWalletCutoverRepository(), + }), + ) + console.log(JSON.stringify(result, null, 2)) + await logProgress(`completed lifecycle state=${result.state}`) +} + +const status = async () => { + const manifest = await loadManifest() + const result = throwIfError( + await CashWalletCutover.getPrimaryCashWalletCutoverStatus({ + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + migrationsRepo: CashWalletCutoverRepository(), + }), + ) + console.log(JSON.stringify(result, null, 2)) +} + +const resetConfig = async () => { + const manifest = await loadManifest() + const result = await CashWalletCutoverConfig.updateOne( + { _id: "cash_wallet_cutover" }, + { + $set: { + state: "pre", + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + updatedBy: "manual-local", + updatedAt: new Date(), + }, + $unset: { + scheduledAt: "", + startedAt: "", + completedAt: "", + pausedAt: "", + pauseReason: "", + }, + }, + { upsert: true }, + ) + console.log(JSON.stringify(result, null, 2)) + await logProgress(`reset singleton cutover config to pre for ${manifest.runId}`) +} + +const verify = async () => { + const manifest = await loadManifest() + const rows = [] + + for (const target of manifest.accounts) { + const account = throwIfError( + await AccountsRepository().findById(target.accountId as AccountId), + ) + const usdBalance = throwIfError( + await getBalanceForWallet({ + walletId: target.legacyUsdWalletId as WalletId, + currency: WalletCurrency.Usd, + }), + ) + const usdtBalance = throwIfError( + await getBalanceForWallet({ + walletId: target.destinationUsdtWalletId as WalletId, + currency: WalletCurrency.Usdt, + }), + ) + + rows.push({ + index: target.index, + accountId: target.accountId, + expectedFundingCents: target.startingFundingCents, + defaultWalletId: account.defaultWalletId, + defaultIsDestinationUsdt: account.defaultWalletId === target.destinationUsdtWalletId, + legacyUsdWalletId: target.legacyUsdWalletId, + destinationUsdtWalletId: target.destinationUsdtWalletId, + legacyUsdBalanceCents: usdBalance.asCents(), + destinationUsdtBalanceMicros: usdtBalance.asSmallestUnits(), + }) + } + + const statusResult = throwIfError( + await CashWalletCutover.getPrimaryCashWalletCutoverStatus({ + cutoverVersion: manifest.cutoverVersion, + runId: manifest.runId, + migrationsRepo: CashWalletCutoverRepository(), + }), + ) + + const result = { status: statusResult, accounts: rows } + await fs.writeFile(RESULTS_PATH, `${JSON.stringify(result, null, 2)}\n`) + console.log(JSON.stringify(result, null, 2)) + await logProgress(`wrote verification results to ${RESULTS_PATH}`) +} + +const commands: Record Promise> = { + "create-accounts": createAccounts, + "complete-partials": completePartialAccounts, + fund: fundAccounts, + preview, + prepare, + start, + "run-batches": runBatches, + complete, + status, + "reset-config": resetConfig, + verify, +} + +setupMongoConnection() + .then(async (mongoose) => { + const command = process.argv.find((arg) => commands[arg]) + if (!command || !commands[command]) { + throw new Error(`Expected command: ${Object.keys(commands).join(", ")}`) + } + + await commands[command]() + await mongoose?.connection.close() + process.exit(0) + }) + .catch(async (error) => { + await logProgress(`ERROR ${error instanceof Error ? error.message : String(error)}`) + console.error(error) + process.exit(1) + }) diff --git a/operator-runs/eng-345-manual-347/task_plan.md b/operator-runs/eng-345-manual-347/task_plan.md new file mode 100644 index 000000000..a7240b9cf --- /dev/null +++ b/operator-runs/eng-345-manual-347/task_plan.md @@ -0,0 +1,70 @@ +# Task Plan: ENG-345 Fresh 7-Account Manual Cutover + +## Goal +Run the full cash-wallet cutover pipeline on the 7 fresh local accounts that were successfully created with legacy USD default wallets. + +## Current Phase +Phase 6 + +## Phases + +### Phase 1: Discovery +- [x] Find existing account creation/funding helpers +- [x] Confirm database and service config for the local run +- [x] Document reusable commands +- **Status:** complete + +### Phase 2: Test Data Setup +- [x] Create 7 new accounts +- [x] Capture account IDs and USD/USDT wallet IDs for 7 complete accounts +- [x] Set 7 complete accounts' `defaultWalletId` to legacy USD wallet ID +- [x] Leave all 7 legacy USD wallets at zero balance +- **Status:** complete + +### Phase 3: Cutover Pipeline +- [x] Preview run +- [x] Prepare run +- [x] Start run +- [x] Run batches until all migrations reach terminal state or failure +- [x] Complete lifecycle if all migrations complete +- **Status:** complete + +### Phase 4: Verification +- [x] Verify migration counts +- [x] Verify account default wallet pointers changed to USDT +- [x] Verify source/destination amounts for zero-balance accounts +- [x] Document any failures and recovery actions +- **Status:** complete + +### Phase 5: Report +- [x] Summarize setup, commands, and final status +- [x] Update session memory +- **Status:** complete + +### Phase 6: Reset and Prep Funded Rerun +- [x] Verify seven funded legacy USD balances +- [x] Reset seven account `defaultWalletId` values to legacy USD wallets +- [x] Create fresh cutover prep run for the same seven accounts +- [x] Verify migration prep count and account pointers +- **Status:** complete + +## Key Parameters +- Worktree: `/Users/dread/Documents/Island-Bitcoin/Flash/flash/.worktrees/eng-345-review` +- Planned cutoverVersion: `347` +- Planned runId: `manual-eng-347` +- Account count: `7` +- Funding: `7 x $0.00` + +## Decisions Made +| Decision | Rationale | +|----------|-----------| +| Use a fresh run/version instead of rewinding manual-eng-346 | manual-eng-346 already moved funds and completed; fresh run gives clean manual-test evidence | +| Convert test to 7 zero-balance accounts | Dread requested changing the plan to a 7-account cutover after IBEX write failures blocked creating/funding 10 accounts | +| Use `cutoverVersion=348`, `runId=manual-eng-348` for the funded rerun prep | Version 347 already completed as the zero-balance cutover, so a new run keeps evidence separated | + +## Errors Encountered +| Error | Attempt | Resolution | +|-------|---------|------------| +| Config loader tried to read `create-accounts` as a YAML file; `Account` model import was undefined | 1 | Patched operator script to locate command anywhere in argv and import `Account` from `@services/mongoose/schema`; rerun with command before `--configPath` | +| IBEX fetch error while creating the 8th account left 7 complete accounts and one partial account without wallets/default | 2 | Reconstructed a manifest for the 7 known-good accounts, excluded the partial account, and patched script to save/resume manifest incrementally | +| IBEX write path continued returning blank `FetchError` after cooldown for partial wallet creation and funding invoice creation | 3 | Stopped rather than running an incomplete/fabricated 10-account cutover; verified reads still work and documented resume state | diff --git a/operator-runs/eng-345-manual-347/verify-348-prep-results.json b/operator-runs/eng-345-manual-347/verify-348-prep-results.json new file mode 100644 index 000000000..cb35272d1 --- /dev/null +++ b/operator-runs/eng-345-manual-347/verify-348-prep-results.json @@ -0,0 +1,74 @@ +{ + "cutoverVersion": 348, + "runId": "manual-eng-348", + "status": { + "config": { + "state": "pre", + "updatedBy": "manual-local", + "cutoverVersion": 348, + "runId": "manual-eng-348", + "updatedAt": "2026-05-23T14:49:36.246Z" + }, + "countsByStatus": { + "not_started": 7 + } + }, + "accounts": [ + { + "index": 1, + "accountId": "6a11ada7e55310755eeb0257", + "defaultWalletId": "0a4d1c55-d8ec-4685-8457-216d41569d61", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 2, + "accountId": "6a11ada8e55310755eeb0271", + "defaultWalletId": "08ee5f13-b4c3-4ff5-835a-55dd786d3887", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 3, + "accountId": "6a11ada9e55310755eeb028b", + "defaultWalletId": "697b8583-6e1d-47d5-aba0-8b2c8aa6bc32", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 4, + "accountId": "6a11ada9e55310755eeb02a5", + "defaultWalletId": "dbdfba0c-ead0-4c94-96f8-e15130b7f796", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 5, + "accountId": "6a11adabe55310755eeb02bf", + "defaultWalletId": "9756a443-d30d-4b1c-a797-933ec2ced1d0", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 6, + "accountId": "6a11adace55310755eeb02d9", + "defaultWalletId": "ecaf103c-d024-419a-8bad-b0c61e8068be", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + }, + { + "index": 7, + "accountId": "6a11adade55310755eeb02f3", + "defaultWalletId": "8a3e1bd4-4d2d-4f97-b201-d58d3723a53c", + "defaultIsLegacyUsd": true, + "legacyUsdBalanceCents": "25", + "destinationUsdtBalanceMicros": "0" + } + ] +} diff --git a/src/app/cash-wallet-cutover/amount-conversion.ts b/src/app/cash-wallet-cutover/amount-conversion.ts new file mode 100644 index 000000000..fd6bd4e09 --- /dev/null +++ b/src/app/cash-wallet-cutover/amount-conversion.ts @@ -0,0 +1,54 @@ +import { InvalidCashWalletCutoverAmountError } from "./errors" + +const USDT_MICROS_PER_USD_CENT = 10_000n + +const parseNonNegativeInteger = ( + value: string, +): bigint | InvalidCashWalletCutoverAmountError => { + if (!/^\d+$/.test(value)) { + return new InvalidCashWalletCutoverAmountError( + `Invalid non-negative integer amount: ${value}`, + ) + } + return BigInt(value) +} + +export const usdCentsToUsdtMicros = ( + usdCents: string, +): string | InvalidCashWalletCutoverAmountError => { + const parsed = parseNonNegativeInteger(usdCents) + if (parsed instanceof Error) return parsed + return (parsed * USDT_MICROS_PER_USD_CENT).toString() +} + +export const feeUsdCentsToUsdtMicros = usdCentsToUsdtMicros + +export const usdtMicrosToUsdCentsCeil = ( + usdtMicros: string, +): string | InvalidCashWalletCutoverAmountError => { + const parsed = parseNonNegativeInteger(usdtMicros) + if (parsed instanceof Error) return parsed + if (parsed === 0n) return "0" + return ((parsed + USDT_MICROS_PER_USD_CENT - 1n) / USDT_MICROS_PER_USD_CENT).toString() +} + +export const destinationShortfallUsdtMicros = ({ + targetUsdtMicros, + startingUsdtMicros, + currentUsdtMicros, +}: { + targetUsdtMicros: string + startingUsdtMicros: string + currentUsdtMicros: string +}): string | InvalidCashWalletCutoverAmountError => { + const target = parseNonNegativeInteger(targetUsdtMicros) + if (target instanceof Error) return target + const starting = parseNonNegativeInteger(startingUsdtMicros) + if (starting instanceof Error) return starting + const current = parseNonNegativeInteger(currentUsdtMicros) + if (current instanceof Error) return current + + const received = current > starting ? current - starting : 0n + if (received >= target) return "0" + return (target - received).toString() +} diff --git a/src/app/cash-wallet-cutover/client-capability.ts b/src/app/cash-wallet-cutover/client-capability.ts new file mode 100644 index 000000000..e30b95fb5 --- /dev/null +++ b/src/app/cash-wallet-cutover/client-capability.ts @@ -0,0 +1,37 @@ +export const CASH_WALLET_USDT_CLIENT_CAPABILITY = "cash-wallet-usdt-v1" + +export type CashWalletPresentation = "legacy_compat" | "usdt" + +export type CashWalletClientCapabilities = { + cashWalletPresentation: CashWalletPresentation + hasUsdtCashWalletSupport: boolean +} + +export const DEFAULT_CASH_WALLET_CLIENT_CAPABILITIES: CashWalletClientCapabilities = { + cashWalletPresentation: "legacy_compat", + hasUsdtCashWalletSupport: false, +} + +export const parseCashWalletClientCapabilities = ( + headers: Record, +): CashWalletClientCapabilities => { + const values = Object.entries(headers).flatMap(([key, raw]) => { + if (key.toLowerCase() !== "x-flash-client-capabilities") return [] + if (typeof raw === "string") return [raw] + if (Array.isArray(raw)) + return raw.filter((value): value is string => typeof value === "string") + return [] + }) + + const capabilities = values + .flatMap((value) => value.split(",")) + .map((value) => value.trim().toLowerCase()) + + const hasUsdtCashWalletSupport = capabilities.includes( + CASH_WALLET_USDT_CLIENT_CAPABILITY, + ) + + if (!hasUsdtCashWalletSupport) return DEFAULT_CASH_WALLET_CLIENT_CAPABILITIES + + return { cashWalletPresentation: "usdt", hasUsdtCashWalletSupport } +} diff --git a/src/app/cash-wallet-cutover/discovery.ts b/src/app/cash-wallet-cutover/discovery.ts new file mode 100644 index 000000000..14304c10b --- /dev/null +++ b/src/app/cash-wallet-cutover/discovery.ts @@ -0,0 +1,78 @@ +import { WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" + +export type CashWalletCutoverDiscoveryStatus = + | "legacy_default" + | "already_usdt" + | "residual_legacy_usd" + | "missing_legacy_usd" + | "missing_destination_usdt" + +export type CashWalletCutoverDiscovery = { + status: CashWalletCutoverDiscoveryStatus + accountId: AccountId + accountUuid?: AccountUuid + legacyUsdWalletId?: WalletId + destinationUsdtWalletId?: WalletId + previousDefaultWalletId: WalletId +} + +export const classifyCashWalletsForCutover = ({ + account, + wallets, +}: { + account: Account + wallets: Wallet[] +}): CashWalletCutoverDiscovery => { + const legacyUsdWallet = wallets.find( + (wallet) => + wallet.type === WalletType.Checking && wallet.currency === WalletCurrency.Usd, + ) + const destinationUsdtWallet = wallets.find( + (wallet) => + wallet.type === WalletType.Checking && wallet.currency === WalletCurrency.Usdt, + ) + + const base = { + accountId: account.id, + accountUuid: account.uuid, + legacyUsdWalletId: legacyUsdWallet?.id, + destinationUsdtWalletId: destinationUsdtWallet?.id, + previousDefaultWalletId: account.defaultWalletId, + } + + if (!legacyUsdWallet) return { ...base, status: "missing_legacy_usd" } + if (!destinationUsdtWallet) return { ...base, status: "missing_destination_usdt" } + + if (account.defaultWalletId === legacyUsdWallet.id) { + return { ...base, status: "legacy_default" } + } + + if (account.defaultWalletId === destinationUsdtWallet.id) { + return { ...base, status: "already_usdt" } + } + + return { ...base, status: "residual_legacy_usd" } +} + +export const discoverCashWalletCutoverAccounts = async ({ + accountsRepo, + walletsRepo, +}: { + accountsRepo: Pick + walletsRepo: Pick +}): Promise => { + const accounts = accountsRepo.listUnlockedAccounts() + if (accounts instanceof Error) return accounts + + const discoveries: CashWalletCutoverDiscovery[] = [] + + for await (const account of accounts) { + const wallets = await walletsRepo.listByAccountId(account.id) + if (wallets instanceof Error) return wallets + + discoveries.push(classifyCashWalletsForCutover({ account, wallets })) + } + + return discoveries +} diff --git a/src/app/cash-wallet-cutover/errors.ts b/src/app/cash-wallet-cutover/errors.ts new file mode 100644 index 000000000..373dcd742 --- /dev/null +++ b/src/app/cash-wallet-cutover/errors.ts @@ -0,0 +1,11 @@ +import { DomainError, ValidationError } from "@domain/shared" + +export class InvalidCashWalletCutoverAmountError extends ValidationError {} +export class InvalidCashWalletMigrationTransitionError extends ValidationError {} +export class InvalidCashWalletCutoverStateTransitionError extends ValidationError {} +export class CashWalletCutoverInProgressError extends ValidationError {} +export class CashWalletMigrationFailedError extends DomainError {} +export class CashWalletMissingLegacyUsdWalletError extends DomainError {} +export class CashWalletMissingUsdtWalletError extends DomainError {} +export class CashWalletCutoverPreflightError extends DomainError {} +export class CashWalletCutoverTreasuryInsufficientBalanceError extends DomainError {} diff --git a/src/app/cash-wallet-cutover/executor.ts b/src/app/cash-wallet-cutover/executor.ts new file mode 100644 index 000000000..5df68aa00 --- /dev/null +++ b/src/app/cash-wallet-cutover/executor.ts @@ -0,0 +1,39 @@ +type RunnableCashWalletMigrationStatus = Exclude< + CashWalletMigrationStatus, + | "complete" + | "failed" + | "requires_operator_review" + | "skipped_already_migrated" + | "rollback_started" + | "rolled_back" +> + +type CashWalletMigrationStepHandler = ( + migration: CashWalletMigration, +) => Promise + +export type CashWalletMigrationStepHandlers = Record< + RunnableCashWalletMigrationStatus, + CashWalletMigrationStepHandler +> + +const terminalStatuses: CashWalletMigrationStatus[] = [ + "complete", + "failed", + "requires_operator_review", + "skipped_already_migrated", + "rollback_started", + "rolled_back", +] + +export const executeCashWalletMigrationStep = async ({ + migration, + handlers, +}: { + migration: CashWalletMigration + handlers: CashWalletMigrationStepHandlers +}): Promise => { + if (terminalStatuses.includes(migration.status)) return migration + + return handlers[migration.status as RunnableCashWalletMigrationStatus](migration) +} diff --git a/src/app/cash-wallet-cutover/guard.ts b/src/app/cash-wallet-cutover/guard.ts new file mode 100644 index 000000000..214968d4c --- /dev/null +++ b/src/app/cash-wallet-cutover/guard.ts @@ -0,0 +1,77 @@ +import { + CashWalletCutoverInProgressError, + CashWalletMigrationFailedError, +} from "./errors" +import { CashWalletClientCapabilities } from "./client-capability" + +export { CashWalletCutoverInProgressError, CashWalletMigrationFailedError } + +export type CashWalletCutoverRoute = "legacy_usd" | "usdt" +export type CashWalletCutoverPresentation = "legacy_usd" | "legacy_usd_compat" | "usdt" + +export type CashWalletCutoverDecision = { + presentation: CashWalletCutoverPresentation +} + +const ACTIVE_STATUSES: CashWalletMigrationStatus[] = [ + "started", + "provisioned", + "balance_read", + "invoice_created", + "balance_move_sending", + "balance_move_sent", + "balance_move_verified", + "fee_reimbursement_invoice_created", + "fee_reimbursement_sending", + "fee_reimbursed", + "pointer_flipped", + "rollback_started", +] + +export const evaluateCashWalletCutoverGuard = ({ + cutover, + migration, +}: { + cutover: CashWalletCutoverConfig + migration?: CashWalletMigration | null +}): { route: CashWalletCutoverRoute } | ApplicationError => { + if (cutover.state === "pre") return { route: "legacy_usd" } + if (cutover.state === "complete") return { route: "usdt" } + + if (!migration || migration.status === "not_started") return { route: "legacy_usd" } + if ( + migration.status === "complete" || + migration.status === "skipped_already_migrated" + ) { + return { route: "usdt" } + } + if (migration.status === "failed" || migration.status === "requires_operator_review") { + return new CashWalletMigrationFailedError() + } + if (ACTIVE_STATUSES.includes(migration.status)) { + return new CashWalletCutoverInProgressError() + } + + return { route: "legacy_usd" } +} + +export const evaluateCashWalletCutoverPresentation = ({ + cutover, + migration, + client, +}: { + cutover: CashWalletCutoverConfig + migration?: CashWalletMigration | null + client: CashWalletClientCapabilities +}): CashWalletCutoverDecision | ApplicationError => { + const guard = evaluateCashWalletCutoverGuard({ cutover, migration }) + if (guard instanceof Error) return guard + + if (guard.route === "legacy_usd") { + return { presentation: "legacy_usd" } + } + + return { + presentation: client.hasUsdtCashWalletSupport ? "usdt" : "legacy_usd_compat", + } +} diff --git a/src/app/cash-wallet-cutover/handlers.ts b/src/app/cash-wallet-cutover/handlers.ts new file mode 100644 index 000000000..20d32ff3f --- /dev/null +++ b/src/app/cash-wallet-cutover/handlers.ts @@ -0,0 +1,173 @@ +import { + completeCashWalletMigration, + createCashWalletMigrationBalanceMoveInvoice, + createCashWalletMigrationFeeReimbursementInvoice, + flipCashWalletMigrationDefaultPointer, + markCashWalletMigrationBalanceMoveSent, + markCashWalletMigrationFeeReimbursed, + provisionCashWalletMigrationDestination, + recordCashWalletMigrationBalance, + sendCashWalletMigrationBalanceMovePayment, + sendCashWalletMigrationFeeReimbursementPayment, + skipCashWalletMigrationFeeReimbursement, + startCashWalletMigration, + verifyCashWalletMigrationBalanceMove, + verifyCashWalletMigrationLegacyZero, +} from "./worker" +import { CashWalletMigrationStepHandlers } from "./executor" + +type CashWalletMigrationTransitionRepository = Parameters< + typeof startCashWalletMigration +>[0]["migrationsRepo"] + +type CashWalletMigrationHandlerServices = { + now(): Date + provisioningService: Parameters< + typeof provisionCashWalletMigrationDestination + >[0]["provisioningService"] + balanceReader: { + readSourceBalanceUsdCents( + migration: CashWalletMigration, + ): Promise + readDestinationBalanceUsdtMicros( + migration: CashWalletMigration, + ): Promise + } + invoiceService: Parameters< + typeof createCashWalletMigrationBalanceMoveInvoice + >[0]["invoiceService"] & + Parameters[0]["invoiceService"] + paymentService: Parameters< + typeof sendCashWalletMigrationBalanceMovePayment + >[0]["paymentService"] + balanceVerifier: Parameters< + typeof verifyCashWalletMigrationBalanceMove + >[0]["balanceVerifier"] + feeService: { + readFeeAmountUsdtMicros( + migration: CashWalletMigration, + ): Promise + } + treasuryService: { + getTreasuryWalletId(): Promise + } + pointerService: Parameters< + typeof flipCashWalletMigrationDefaultPointer + >[0]["pointerService"] + legacyWalletVerifier: Parameters< + typeof verifyCashWalletMigrationLegacyZero + >[0]["legacyWalletVerifier"] +} + +export const createCashWalletMigrationStepHandlers = ({ + migrationsRepo, + services, +}: { + migrationsRepo: CashWalletMigrationTransitionRepository + services: CashWalletMigrationHandlerServices +}): CashWalletMigrationStepHandlers => ({ + not_started: (migration) => + startCashWalletMigration({ + migration, + migrationsRepo, + startedAt: services.now(), + }), + started: (migration) => + provisionCashWalletMigrationDestination({ + migration, + migrationsRepo, + provisioningService: services.provisioningService, + }), + provisioned: async (migration) => { + const sourceBalanceUsdCents = + await services.balanceReader.readSourceBalanceUsdCents(migration) + if (sourceBalanceUsdCents instanceof Error) return sourceBalanceUsdCents + const destinationStartingBalanceUsdtMicros = + await services.balanceReader.readDestinationBalanceUsdtMicros(migration) + if (destinationStartingBalanceUsdtMicros instanceof Error) { + return destinationStartingBalanceUsdtMicros + } + return recordCashWalletMigrationBalance({ + migration, + migrationsRepo, + sourceBalanceUsdCents, + destinationStartingBalanceUsdtMicros, + }) + }, + balance_read: (migration) => { + if (migration.destinationAmountUsdtMicros === "0") { + return flipCashWalletMigrationDefaultPointer({ + migration, + migrationsRepo, + pointerService: services.pointerService, + }) + } + + return createCashWalletMigrationBalanceMoveInvoice({ + migration, + migrationsRepo, + invoiceService: services.invoiceService, + }) + }, + invoice_created: (migration) => + sendCashWalletMigrationBalanceMovePayment({ + migration, + migrationsRepo, + paymentService: services.paymentService, + }), + balance_move_sending: (migration) => + markCashWalletMigrationBalanceMoveSent({ migration, migrationsRepo }), + balance_move_sent: (migration) => + verifyCashWalletMigrationBalanceMove({ + migration, + migrationsRepo, + balanceVerifier: services.balanceVerifier, + }), + balance_move_verified: async (migration) => { + const feeAmountUsdtMicros = + await services.feeService.readFeeAmountUsdtMicros(migration) + if (feeAmountUsdtMicros instanceof Error) return feeAmountUsdtMicros + if (feeAmountUsdtMicros === "0") { + return skipCashWalletMigrationFeeReimbursement({ + migration, + migrationsRepo, + }) + } + return createCashWalletMigrationFeeReimbursementInvoice({ + migration, + migrationsRepo, + invoiceService: services.invoiceService, + feeAmountUsdtMicros, + }) + }, + fee_reimbursement_invoice_created: async (migration) => { + const treasuryWalletId = await services.treasuryService.getTreasuryWalletId() + if (treasuryWalletId instanceof Error) return treasuryWalletId + return sendCashWalletMigrationFeeReimbursementPayment({ + migration, + migrationsRepo, + paymentService: services.paymentService, + treasuryWalletId, + }) + }, + fee_reimbursement_sending: (migration) => + markCashWalletMigrationFeeReimbursed({ migration, migrationsRepo }), + fee_reimbursed: (migration) => + flipCashWalletMigrationDefaultPointer({ + migration, + migrationsRepo, + pointerService: services.pointerService, + }), + pointer_flipped: (migration) => + verifyCashWalletMigrationLegacyZero({ + migration, + migrationsRepo, + legacyWalletVerifier: services.legacyWalletVerifier, + }), + legacy_zero_verified: (migration) => + completeCashWalletMigration({ + migration, + migrationsRepo, + completedAt: services.now(), + }), +}) diff --git a/src/app/cash-wallet-cutover/index.ts b/src/app/cash-wallet-cutover/index.ts new file mode 100644 index 000000000..eae3f76bd --- /dev/null +++ b/src/app/cash-wallet-cutover/index.ts @@ -0,0 +1,23 @@ +export * from "./amount-conversion" +export * from "./client-capability" +export * from "./errors" +export * from "./presentation" +export * from "./presentation-for-account" +export * from "./state-machine" +export { + evaluateCashWalletCutoverGuard, + evaluateCashWalletCutoverPresentation, +} from "./guard" +export * from "./discovery" +export * from "./preflight" +export * from "./planner" +export * from "./migration-records" +export * from "./prepare" +export * from "./worker" +export * from "./executor" +export * from "./runner" +export * from "./handlers" +export * from "./runtime-services" +export * from "./orchestrator" +export * from "./lifecycle" +export * from "./preview" diff --git a/src/app/cash-wallet-cutover/index.types.d.ts b/src/app/cash-wallet-cutover/index.types.d.ts new file mode 100644 index 000000000..3f16650b7 --- /dev/null +++ b/src/app/cash-wallet-cutover/index.types.d.ts @@ -0,0 +1,67 @@ +type CashWalletCutoverState = "pre" | "in_progress" | "complete" + +type CashWalletMigrationStatus = + | "not_started" + | "started" + | "provisioned" + | "balance_read" + | "invoice_created" + | "balance_move_sending" + | "balance_move_sent" + | "balance_move_verified" + | "fee_reimbursement_invoice_created" + | "fee_reimbursement_sending" + | "fee_reimbursed" + | "pointer_flipped" + | "legacy_zero_verified" + | "complete" + | "failed" + | "requires_operator_review" + | "skipped_already_migrated" + | "rollback_started" + | "rolled_back" + +type CashWalletCutoverConfig = { + state: CashWalletCutoverState + scheduledAt?: Date + startedAt?: Date + completedAt?: Date + pausedAt?: Date + pauseReason?: string + updatedBy?: string + cutoverVersion: number + runId?: string + updatedAt: Date +} + +type CashWalletMigration = { + id: string + accountId: AccountId + accountUuid?: AccountUuid + legacyUsdWalletId: WalletId + destinationUsdtWalletId: WalletId + previousDefaultWalletId?: WalletId + cutoverVersion: number + runId: string + status: CashWalletMigrationStatus + sourceBalanceUsdCents?: string + destinationAmountUsdtMicros?: string + destinationStartingBalanceUsdtMicros?: string + feeAmountUsdCents?: string + feeAmountUsdtMicros?: string + balanceMoveInvoicePaymentRequest?: string + balanceMoveInvoicePaymentHash?: string + balanceMovePaymentTransactionId?: string + feeReimbursementInvoicePaymentRequest?: string + feeReimbursementInvoicePaymentHash?: string + feeReimbursementPaymentTransactionId?: string + estimatedFee?: boolean + idempotencyKey: string + attempts: number + lastError?: string + lockedAt?: Date + lockedBy?: string + startedAt?: Date + completedAt?: Date + updatedAt: Date +} diff --git a/src/app/cash-wallet-cutover/lifecycle.ts b/src/app/cash-wallet-cutover/lifecycle.ts new file mode 100644 index 000000000..80e4fd848 --- /dev/null +++ b/src/app/cash-wallet-cutover/lifecycle.ts @@ -0,0 +1,174 @@ +import { CashWalletCutoverRepository } from "@services/mongoose" + +import { + CashWalletCutoverInProgressError, + CashWalletMigrationFailedError, + InvalidCashWalletCutoverStateTransitionError, +} from "./errors" + +const migrationStatuses: CashWalletMigrationStatus[] = [ + "not_started", + "started", + "provisioned", + "balance_read", + "invoice_created", + "balance_move_sending", + "balance_move_sent", + "balance_move_verified", + "fee_reimbursement_invoice_created", + "fee_reimbursement_sending", + "fee_reimbursed", + "pointer_flipped", + "legacy_zero_verified", + "complete", + "failed", + "requires_operator_review", + "skipped_already_migrated", + "rollback_started", + "rolled_back", +] + +type CashWalletCutoverLifecycleRepository = { + getConfig: () => Promise + updateConfig: ( + patch: Partial, + actor?: string, + ) => Promise + listRunnableMigrations: ({ + cutoverVersion, + runId, + limit, + }: { + cutoverVersion: number + runId: string + limit?: number + }) => Promise + countByStatus: ({ + cutoverVersion, + runId, + status, + }: { + cutoverVersion: number + runId: string + status: CashWalletMigrationStatus + }) => Promise +} + +export type CashWalletCutoverStatusReport = { + config: CashWalletCutoverConfig + countsByStatus: Partial> +} + +export const startPrimaryCashWalletCutover = async ({ + cutoverVersion, + runId, + actor, + now = new Date(), + migrationsRepo = CashWalletCutoverRepository(), +}: { + cutoverVersion: number + runId: string + actor: string + now?: Date + migrationsRepo?: CashWalletCutoverLifecycleRepository +}): Promise => { + const config = await migrationsRepo.getConfig() + if (config instanceof Error) return config + + if (config.state === "complete") { + return new InvalidCashWalletCutoverStateTransitionError( + "Cash wallet cutover is already complete", + ) + } + + if (config.state === "in_progress") { + if (config.runId === runId && config.cutoverVersion === cutoverVersion) return config + return new CashWalletCutoverInProgressError( + "Cash wallet cutover is already in progress", + ) + } + + return migrationsRepo.updateConfig( + { + state: "in_progress", + cutoverVersion, + runId, + startedAt: now, + scheduledAt: undefined, + pausedAt: undefined, + pauseReason: undefined, + }, + actor, + ) +} + +export const completePrimaryCashWalletCutover = async ({ + cutoverVersion, + runId, + actor, + now = new Date(), + migrationsRepo = CashWalletCutoverRepository(), +}: { + cutoverVersion: number + runId: string + actor: string + now?: Date + migrationsRepo?: CashWalletCutoverLifecycleRepository +}): Promise => { + const failedCount = await migrationsRepo.countByStatus({ + cutoverVersion, + runId, + status: "failed", + }) + if (failedCount instanceof Error) return failedCount + if (failedCount > 0) return new CashWalletMigrationFailedError() + + const reviewCount = await migrationsRepo.countByStatus({ + cutoverVersion, + runId, + status: "requires_operator_review", + }) + if (reviewCount instanceof Error) return reviewCount + if (reviewCount > 0) return new CashWalletMigrationFailedError() + + const runnable = await migrationsRepo.listRunnableMigrations({ + cutoverVersion, + runId, + limit: 1, + }) + if (runnable instanceof Error) return runnable + if (runnable.length > 0) return new CashWalletCutoverInProgressError() + + return migrationsRepo.updateConfig( + { + state: "complete", + cutoverVersion, + runId, + completedAt: now, + }, + actor, + ) +} + +export const getPrimaryCashWalletCutoverStatus = async ({ + cutoverVersion, + runId, + migrationsRepo = CashWalletCutoverRepository(), +}: { + cutoverVersion: number + runId: string + migrationsRepo?: CashWalletCutoverLifecycleRepository +}): Promise => { + const config = await migrationsRepo.getConfig() + if (config instanceof Error) return config + + const countsByStatus: Partial> = {} + + for (const status of migrationStatuses) { + const count = await migrationsRepo.countByStatus({ cutoverVersion, runId, status }) + if (count instanceof Error) return count + if (count > 0) countsByStatus[status] = count + } + + return { config, countsByStatus } +} diff --git a/src/app/cash-wallet-cutover/migration-records.ts b/src/app/cash-wallet-cutover/migration-records.ts new file mode 100644 index 000000000..9ac33b8d0 --- /dev/null +++ b/src/app/cash-wallet-cutover/migration-records.ts @@ -0,0 +1,28 @@ +import { PrimaryCashWalletMigrationPlan } from "./planner" + +type CashWalletMigrationRecordsRepository = { + upsertMigration( + args: PrimaryCashWalletMigrationPlan, + ): Promise +} + +export type { CashWalletMigrationRecordsRepository } + +export const upsertPrimaryCashWalletMigrationRecords = async ({ + migrationsRepo, + plans, +}: { + migrationsRepo: CashWalletMigrationRecordsRepository + plans: PrimaryCashWalletMigrationPlan[] +}): Promise => { + const migrations: CashWalletMigration[] = [] + + for (const plan of plans) { + const migration = await migrationsRepo.upsertMigration(plan) + if (migration instanceof Error) return migration + + migrations.push(migration) + } + + return migrations +} diff --git a/src/app/cash-wallet-cutover/orchestrator.ts b/src/app/cash-wallet-cutover/orchestrator.ts new file mode 100644 index 000000000..89d8203c6 --- /dev/null +++ b/src/app/cash-wallet-cutover/orchestrator.ts @@ -0,0 +1,52 @@ +import { CashWalletCutoverRepository } from "@services/mongoose" + +import { createCashWalletMigrationStepHandlers } from "./handlers" +import { createCashWalletMigrationRuntimeServices } from "./runtime-services" +import { executeCashWalletMigrationStep } from "./executor" +import { runCashWalletMigrationBatch } from "./runner" + +type PrimaryCashWalletCutoverBatchRepository = Parameters< + typeof runCashWalletMigrationBatch +>[0]["migrationsRepo"] & + Parameters[0]["migrationsRepo"] + +type PrimaryCashWalletCutoverRuntimeServices = Parameters< + typeof createCashWalletMigrationStepHandlers +>[0]["services"] + +export const runPrimaryCashWalletCutoverBatch = ({ + cutoverVersion, + runId, + workerId, + limit, + lockStaleBefore, + migrationsRepo = CashWalletCutoverRepository(), + runtimeServices = createCashWalletMigrationRuntimeServices(), +}: { + cutoverVersion: number + runId: string + workerId: string + limit?: number + lockStaleBefore: Date + migrationsRepo?: PrimaryCashWalletCutoverBatchRepository + runtimeServices?: PrimaryCashWalletCutoverRuntimeServices +}) => { + const handlers = createCashWalletMigrationStepHandlers({ + migrationsRepo, + services: runtimeServices, + }) + + return runCashWalletMigrationBatch({ + cutoverVersion, + runId, + workerId, + limit, + lockStaleBefore, + migrationsRepo, + executor: (migration) => + executeCashWalletMigrationStep({ + migration, + handlers, + }), + }) +} diff --git a/src/app/cash-wallet-cutover/planner.ts b/src/app/cash-wallet-cutover/planner.ts new file mode 100644 index 000000000..0a1e2031c --- /dev/null +++ b/src/app/cash-wallet-cutover/planner.ts @@ -0,0 +1,41 @@ +import { CashWalletCutoverDiscovery } from "./discovery" + +type PrimaryCashWalletMigrationPlan = { + accountId: AccountId + accountUuid?: AccountUuid + legacyUsdWalletId: WalletId + destinationUsdtWalletId: WalletId + previousDefaultWalletId: WalletId + cutoverVersion: number + runId: string + idempotencyKey: string +} + +export type { PrimaryCashWalletMigrationPlan } + +export const buildPrimaryCashWalletMigrationPlan = ({ + cutoverVersion, + runId, + discoveries, +}: { + cutoverVersion: number + runId: string + discoveries: CashWalletCutoverDiscovery[] +}): PrimaryCashWalletMigrationPlan[] => + discoveries.flatMap((discovery) => { + if (discovery.status !== "legacy_default") return [] + if (!discovery.legacyUsdWalletId || !discovery.destinationUsdtWalletId) return [] + + return [ + { + accountId: discovery.accountId, + accountUuid: discovery.accountUuid, + legacyUsdWalletId: discovery.legacyUsdWalletId, + destinationUsdtWalletId: discovery.destinationUsdtWalletId, + previousDefaultWalletId: discovery.previousDefaultWalletId, + cutoverVersion, + runId, + idempotencyKey: `cash-wallet-cutover:${runId}:${discovery.accountId}`, + }, + ] + }) diff --git a/src/app/cash-wallet-cutover/preflight.ts b/src/app/cash-wallet-cutover/preflight.ts new file mode 100644 index 000000000..df05bfddf --- /dev/null +++ b/src/app/cash-wallet-cutover/preflight.ts @@ -0,0 +1,53 @@ +import { CashWalletCutoverDiscovery } from "./discovery" + +type CashWalletCutoverPreflightBlocker = { + accountId: AccountId + reason: "missing_legacy_usd" | "missing_destination_usdt" +} + +type CashWalletCutoverPreflightReport = { + cutoverVersion: number + runId: string + totalAccounts: number + migrationCandidates: number + alreadyUsdt: number + residualLegacyUsd: number + blockers: number + blockerAccounts: CashWalletCutoverPreflightBlocker[] + canStart: boolean +} + +export type { CashWalletCutoverPreflightReport } + +export const buildCashWalletCutoverPreflightReport = ({ + cutoverVersion, + runId, + discoveries, +}: { + cutoverVersion: number + runId: string + discoveries: CashWalletCutoverDiscovery[] +}): CashWalletCutoverPreflightReport => { + const blockerAccounts = discoveries.flatMap(({ accountId, status }) => { + if (status !== "missing_legacy_usd" && status !== "missing_destination_usdt") { + return [] + } + + return [{ accountId, reason: status }] + }) + + return { + cutoverVersion, + runId, + totalAccounts: discoveries.length, + migrationCandidates: discoveries.filter(({ status }) => status === "legacy_default") + .length, + alreadyUsdt: discoveries.filter(({ status }) => status === "already_usdt").length, + residualLegacyUsd: discoveries.filter( + ({ status }) => status === "residual_legacy_usd", + ).length, + blockers: blockerAccounts.length, + blockerAccounts, + canStart: blockerAccounts.length === 0, + } +} diff --git a/src/app/cash-wallet-cutover/prepare.ts b/src/app/cash-wallet-cutover/prepare.ts new file mode 100644 index 000000000..35793b208 --- /dev/null +++ b/src/app/cash-wallet-cutover/prepare.ts @@ -0,0 +1,63 @@ +import { + buildCashWalletCutoverPreflightReport, + CashWalletCutoverPreflightReport, +} from "./preflight" +import { discoverCashWalletCutoverAccounts } from "./discovery" +import { + buildPrimaryCashWalletMigrationPlan, + PrimaryCashWalletMigrationPlan, +} from "./planner" +import { + upsertPrimaryCashWalletMigrationRecords, + CashWalletMigrationRecordsRepository, +} from "./migration-records" + +type PreparePrimaryCashWalletCutoverResult = { + report: CashWalletCutoverPreflightReport + plannedMigrations: PrimaryCashWalletMigrationPlan[] + migrations: CashWalletMigration[] +} + +export const preparePrimaryCashWalletCutover = async ({ + cutoverVersion, + runId, + accountsRepo, + walletsRepo, + migrationsRepo, +}: { + cutoverVersion: number + runId: string + accountsRepo: Pick + walletsRepo: Pick + migrationsRepo: CashWalletMigrationRecordsRepository +}): Promise => { + const discoveries = await discoverCashWalletCutoverAccounts({ + accountsRepo, + walletsRepo, + }) + if (discoveries instanceof Error) return discoveries + + const report = buildCashWalletCutoverPreflightReport({ + cutoverVersion, + runId, + discoveries, + }) + + if (!report.canStart) { + return { report, plannedMigrations: [], migrations: [] } + } + + const plannedMigrations = buildPrimaryCashWalletMigrationPlan({ + cutoverVersion, + runId, + discoveries, + }) + + const migrations = await upsertPrimaryCashWalletMigrationRecords({ + migrationsRepo, + plans: plannedMigrations, + }) + if (migrations instanceof Error) return migrations + + return { report, plannedMigrations, migrations } +} diff --git a/src/app/cash-wallet-cutover/presentation-for-account.ts b/src/app/cash-wallet-cutover/presentation-for-account.ts new file mode 100644 index 000000000..53483ea19 --- /dev/null +++ b/src/app/cash-wallet-cutover/presentation-for-account.ts @@ -0,0 +1,94 @@ +import { WalletsRepository, CashWalletCutoverRepository } from "@services/mongoose" + +import { CashWalletClientCapabilities } from "./client-capability" +import { CashWalletCutoverPreflightError } from "./errors" +import { evaluateCashWalletCutoverPresentation } from "./guard" +import { + CashWalletPresentationResult, + resolveCashWalletPresentation, +} from "./presentation" + +type CashWalletPresentationMigrationsRepository = { + getConfig: () => Promise + findMigrationByAccountId: ({ + accountId, + cutoverVersion, + runId, + }: { + accountId: AccountId + cutoverVersion: number + runId: string + }) => Promise +} + +type CashWalletPresentationWalletsRepository = { + listByAccountId: (accountId: AccountId) => Promise +} + +export const resolveCashWalletPresentationForAccount = async ({ + account, + client, + migrationsRepo = CashWalletCutoverRepository(), + walletsRepo = WalletsRepository(), +}: { + account: Account + client: CashWalletClientCapabilities + migrationsRepo?: CashWalletPresentationMigrationsRepository + walletsRepo?: CashWalletPresentationWalletsRepository +}): Promise => { + const cutover = await migrationsRepo.getConfig() + if (cutover instanceof Error) return cutover + + let migration: CashWalletMigration | null | undefined + if (cutover.state === "in_progress") { + if (!cutover.runId) return new CashWalletCutoverPreflightError() + + const foundMigration = await migrationsRepo.findMigrationByAccountId({ + accountId: account.id, + cutoverVersion: cutover.cutoverVersion, + runId: cutover.runId, + }) + if (foundMigration instanceof Error) return foundMigration + migration = foundMigration + } + + const decision = evaluateCashWalletCutoverPresentation({ + cutover, + migration, + client, + }) + if (decision instanceof Error) return decision + + const wallets = await walletsRepo.listByAccountId(account.id) + if (wallets instanceof Error) return wallets + + return resolveCashWalletPresentation({ decision, wallets }) +} + +export const resolveCashWalletMutationWalletIdForAccount = async ({ + account, + walletId, + client, + migrationsRepo, + walletsRepo, +}: { + account: Account + walletId: WalletId + client: CashWalletClientCapabilities + migrationsRepo?: CashWalletPresentationMigrationsRepository + walletsRepo?: CashWalletPresentationWalletsRepository +}): Promise => { + const presentation = await resolveCashWalletPresentationForAccount({ + account, + client, + migrationsRepo, + walletsRepo, + }) + if (presentation instanceof Error) return presentation + + if (walletId === presentation.legacyUsdWallet?.id) { + return presentation.activeSettlementWallet.id + } + + return walletId +} diff --git a/src/app/cash-wallet-cutover/presentation.ts b/src/app/cash-wallet-cutover/presentation.ts new file mode 100644 index 000000000..7c41478a8 --- /dev/null +++ b/src/app/cash-wallet-cutover/presentation.ts @@ -0,0 +1,82 @@ +import { WalletCurrency } from "@domain/shared" + +import { CashWalletCutoverDecision } from "./guard" +import { + CashWalletMissingLegacyUsdWalletError, + CashWalletMissingUsdtWalletError, +} from "./errors" + +export type CashWalletPresentationResult = { + wallets: Wallet[] + defaultWalletId: WalletId + legacyUsdWallet?: Wallet + activeSettlementWallet: Wallet +} + +export const resolveCashWalletPresentation = ({ + decision, + wallets, +}: { + decision: CashWalletCutoverDecision + wallets: Wallet[] +}): CashWalletPresentationResult | ApplicationError => { + const legacyUsdWallet = wallets.find((wallet) => wallet.currency === WalletCurrency.Usd) + const usdtWallet = wallets.find((wallet) => wallet.currency === WalletCurrency.Usdt) + const nonCashWallets = wallets.filter( + (wallet) => + wallet.currency !== WalletCurrency.Usd && wallet.currency !== WalletCurrency.Usdt, + ) + + if (decision.presentation === "usdt") { + if (!usdtWallet) return new CashWalletMissingUsdtWalletError() + + return { + wallets: [...nonCashWallets, usdtWallet], + defaultWalletId: usdtWallet.id, + legacyUsdWallet, + activeSettlementWallet: usdtWallet, + } + } + + if (!legacyUsdWallet) return new CashWalletMissingLegacyUsdWalletError() + + if (decision.presentation === "legacy_usd_compat") { + if (!usdtWallet) return new CashWalletMissingUsdtWalletError() + + return { + wallets: [...nonCashWallets, legacyUsdWallet], + defaultWalletId: legacyUsdWallet.id, + legacyUsdWallet, + activeSettlementWallet: usdtWallet, + } + } + + return { + wallets: [...nonCashWallets, legacyUsdWallet], + defaultWalletId: legacyUsdWallet.id, + legacyUsdWallet, + activeSettlementWallet: legacyUsdWallet, + } +} + +export const cashWalletTransactionWalletIdsForPresentation = ({ + walletIds, + presentation, +}: { + walletIds?: WalletId[] + presentation: CashWalletPresentationResult +}): WalletId[] => { + const selectedWalletIds = walletIds ?? presentation.wallets.map((wallet) => wallet.id) + + if (!presentation.legacyUsdWallet) return selectedWalletIds + + return Array.from( + new Set( + selectedWalletIds.map((walletId) => + walletId === presentation.legacyUsdWallet?.id + ? presentation.activeSettlementWallet.id + : walletId, + ), + ), + ) +} diff --git a/src/app/cash-wallet-cutover/preview.ts b/src/app/cash-wallet-cutover/preview.ts new file mode 100644 index 000000000..2cca59bc9 --- /dev/null +++ b/src/app/cash-wallet-cutover/preview.ts @@ -0,0 +1,54 @@ +import { AccountsRepository, WalletsRepository } from "@services/mongoose" + +import { discoverCashWalletCutoverAccounts } from "./discovery" +import { + buildCashWalletCutoverPreflightReport, + CashWalletCutoverPreflightReport, +} from "./preflight" +import { + buildPrimaryCashWalletMigrationPlan, + PrimaryCashWalletMigrationPlan, +} from "./planner" + +export const previewPrimaryCashWalletCutover = async ({ + cutoverVersion, + runId, + accountsRepo = AccountsRepository(), + walletsRepo = WalletsRepository(), +}: { + cutoverVersion: number + runId: string + accountsRepo?: Pick + walletsRepo?: Pick +}): Promise< + | { + report: CashWalletCutoverPreflightReport + plannedMigrations: PrimaryCashWalletMigrationPlan[] + } + | RepositoryError +> => { + const discoveries = await discoverCashWalletCutoverAccounts({ + accountsRepo, + walletsRepo, + }) + if (discoveries instanceof Error) return discoveries + + const report = buildCashWalletCutoverPreflightReport({ + cutoverVersion, + runId, + discoveries, + }) + + if (!report.canStart) { + return { report, plannedMigrations: [] } + } + + return { + report, + plannedMigrations: buildPrimaryCashWalletMigrationPlan({ + cutoverVersion, + runId, + discoveries, + }), + } +} diff --git a/src/app/cash-wallet-cutover/runner.ts b/src/app/cash-wallet-cutover/runner.ts new file mode 100644 index 000000000..c937496f0 --- /dev/null +++ b/src/app/cash-wallet-cutover/runner.ts @@ -0,0 +1,129 @@ +type CashWalletMigrationBatchRepository = { + listRunnableMigrations(args: { + cutoverVersion: number + runId: string + limit?: number + }): Promise + acquireMigrationLock(args: { + id: string + workerId: string + staleBefore: Date + cutoverVersion: number + runId: string + }): Promise + releaseMigrationLock(args: { + id: string + workerId: string + cutoverVersion: number + runId: string + }): Promise + markMigrationFailed(args: { + id: string + workerId: string + cutoverVersion: number + runId: string + error: Error + status: "failed" | "requires_operator_review" + }): Promise +} + +type CashWalletMigrationBatchExecutor = ( + migration: CashWalletMigration, +) => Promise + +type CashWalletMigrationBatchResult = { + attempted: number + advanced: number + failed: number + skipped: number +} + +const AMBIGUOUS_SIDE_EFFECT_STATUSES: CashWalletMigrationStatus[] = [ + "invoice_created", + "balance_move_sending", + "balance_move_sent", + "balance_move_verified", + "fee_reimbursement_invoice_created", + "fee_reimbursement_sending", + "fee_reimbursed", + "pointer_flipped", +] + +const failureStatusForMigration = ( + status: CashWalletMigrationStatus, +): "failed" | "requires_operator_review" => + AMBIGUOUS_SIDE_EFFECT_STATUSES.includes(status) ? "requires_operator_review" : "failed" + +export const runCashWalletMigrationBatch = async ({ + cutoverVersion, + runId, + workerId, + limit, + lockStaleBefore, + migrationsRepo, + executor, +}: { + cutoverVersion: number + runId: string + workerId: string + limit?: number + lockStaleBefore: Date + migrationsRepo: CashWalletMigrationBatchRepository + executor: CashWalletMigrationBatchExecutor +}): Promise => { + const migrations = await migrationsRepo.listRunnableMigrations({ + cutoverVersion, + runId, + limit, + }) + if (migrations instanceof Error) return migrations + + const result: CashWalletMigrationBatchResult = { + attempted: 0, + advanced: 0, + failed: 0, + skipped: 0, + } + + for (const migration of migrations) { + result.attempted += 1 + + const locked = await migrationsRepo.acquireMigrationLock({ + id: migration.id, + workerId, + staleBefore: lockStaleBefore, + cutoverVersion, + runId, + }) + if (locked instanceof Error) { + result.skipped += 1 + continue + } + + const step = await executor(locked) + if (step instanceof Error) { + result.failed += 1 + const marked = await migrationsRepo.markMigrationFailed({ + id: locked.id, + workerId, + cutoverVersion, + runId, + error: step, + status: failureStatusForMigration(locked.status), + }) + if (marked instanceof Error) return marked + continue + } else { + result.advanced += 1 + } + + await migrationsRepo.releaseMigrationLock({ + id: locked.id, + workerId, + cutoverVersion, + runId, + }) + } + + return result +} diff --git a/src/app/cash-wallet-cutover/runtime-services.ts b/src/app/cash-wallet-cutover/runtime-services.ts new file mode 100644 index 000000000..776149ba5 --- /dev/null +++ b/src/app/cash-wallet-cutover/runtime-services.ts @@ -0,0 +1,266 @@ +import { addWalletIfNonexistent, updateDefaultWalletId } from "@app/accounts" +import { + addInvoiceForRecipientForUsdWallet, + getBalanceForWallet, +} from "@app/wallets" +import { decodeInvoice } from "@domain/bitcoin/lightning" +import { InvalidWalletId } from "@domain/errors" +import { USDAmount, USDTAmount, WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" +import { AccountsRepository } from "@services/mongoose" +import Ibex from "@services/ibex/client" +import { UnexpectedIbexResponse } from "@services/ibex/errors" +import { getFunderWalletId } from "@services/ledger/caching" + +import { + CashWalletMigrationFailedError, + InvalidCashWalletCutoverAmountError, + InvalidCashWalletMigrationTransitionError, +} from "./errors" +import { destinationShortfallUsdtMicros } from "./amount-conversion" + +const CUTOVER_IBEX_INVOICE_EXPIRATION_SECONDS = 15 * 60 + +type RuntimeServiceDependencies = { + now?: () => Date + addWalletIfNonexistent?: typeof addWalletIfNonexistent + updateDefaultWalletId?: typeof updateDefaultWalletId + getBalanceForWallet?: typeof getBalanceForWallet + createInvoice?: typeof addInvoiceForRecipientForUsdWallet + createNoAmountInvoice?: typeof Ibex.addInvoice + payInvoice?: typeof Ibex.payInvoice + accountsRepo?: Pick, "findById"> + getTreasuryWalletId?: () => Promise +} + +const isUsdAmount = (amount: unknown): amount is USDAmount => amount instanceof USDAmount +const isUsdtAmount = (amount: unknown): amount is USDTAmount => + amount instanceof USDTAmount + +const ibexInvoiceToDomainInvoice = (response: Awaited>) => { + if (response instanceof Error) return response + + const invoiceString = response.invoice?.bolt11 + if (!invoiceString) return new UnexpectedIbexResponse("Could not find invoice.") + + const decodedInvoice = decodeInvoice(invoiceString) + if (decodedInvoice instanceof Error) return decodedInvoice + + return decodedInvoice +} + +export const createCashWalletMigrationRuntimeServices = ( + deps: RuntimeServiceDependencies = {}, +) => { + const addWallet = deps.addWalletIfNonexistent ?? addWalletIfNonexistent + const updateDefaultWallet = deps.updateDefaultWalletId ?? updateDefaultWalletId + const balanceForWallet = deps.getBalanceForWallet ?? getBalanceForWallet + const invoiceForRecipient = deps.createInvoice ?? addInvoiceForRecipientForUsdWallet + const noAmountInvoiceForRecipient = deps.createNoAmountInvoice ?? Ibex.addInvoice + const payInvoice = deps.payInvoice ?? Ibex.payInvoice + const accountsRepo = deps.accountsRepo ?? AccountsRepository() + + return { + now: deps.now ?? (() => new Date()), + provisioningService: { + ensureDestinationWallet: async ({ + accountId, + destinationUsdtWalletId, + }: { + accountId: AccountId + destinationUsdtWalletId: WalletId + }): Promise => { + const wallet = await addWallet({ + accountId, + type: WalletType.Checking, + currency: WalletCurrency.Usdt, + }) + if (wallet instanceof Error) return wallet + if (wallet.id !== destinationUsdtWalletId) return new InvalidWalletId() + return true + }, + }, + balanceReader: { + readSourceBalanceUsdCents: async ( + migration: CashWalletMigration, + ): Promise => { + const balance = await balanceForWallet({ + walletId: migration.legacyUsdWalletId, + currency: WalletCurrency.Usd, + }) + if (balance instanceof Error) return balance + if (!isUsdAmount(balance)) { + return new InvalidCashWalletCutoverAmountError("Expected USD balance") + } + return balance.asCents() + }, + readDestinationBalanceUsdtMicros: async ( + migration: CashWalletMigration, + ): Promise => { + const balance = await balanceForWallet({ + walletId: migration.destinationUsdtWalletId, + currency: WalletCurrency.Usdt, + }) + if (balance instanceof Error) return balance + if (!isUsdtAmount(balance)) { + return new InvalidCashWalletCutoverAmountError("Expected USDT balance") + } + return balance.asSmallestUnits() + }, + }, + invoiceService: { + createInvoice: ({ + recipientWalletId, + amount, + memo, + }: { + recipientWalletId: WalletId + amount: string + memo: string + }) => + invoiceForRecipient({ + recipientWalletId, + amount: amount as FractionalCentAmount, + memo, + }), + createNoAmountInvoice: ({ + recipientWalletId, + memo, + }: { + recipientWalletId: WalletId + memo: string + }) => + noAmountInvoiceForRecipient({ + accountId: recipientWalletId, + memo, + expiration: CUTOVER_IBEX_INVOICE_EXPIRATION_SECONDS as Seconds, + }).then(ibexInvoiceToDomainInvoice), + }, + paymentService: { + payInvoice: async ({ + senderWalletId, + paymentRequest, + senderAmountUsdCents, + }: { + senderWalletId: WalletId + paymentRequest: string + senderAmountUsdCents?: string + }): Promise<{ transactionId: IbexTransactionId } | ApplicationError> => { + const send = + senderAmountUsdCents === undefined + ? undefined + : USDAmount.cents(senderAmountUsdCents) + if (send instanceof Error) return send + + const payment = await payInvoice({ + accountId: senderWalletId as IbexAccountId, + invoice: paymentRequest as Bolt11, + send, + }) + if (payment instanceof Error) return payment + + const transactionId = payment.transaction?.id + if (!transactionId) { + return new UnexpectedIbexResponse("Payment transaction id not found") + } + return { transactionId: transactionId as IbexTransactionId } + }, + }, + balanceVerifier: { + verifyBalanceMove: async ({ + legacyUsdWalletId, + }: { + legacyUsdWalletId: WalletId + }): Promise => { + const balance = await balanceForWallet({ + walletId: legacyUsdWalletId, + currency: WalletCurrency.Usd, + }) + if (balance instanceof Error) return balance + if (!isUsdAmount(balance) || !balance.isZero()) { + return new CashWalletMigrationFailedError("Legacy USD wallet is not zero") + } + return true + }, + }, + feeService: { + readFeeAmountUsdtMicros: async ( + migration: CashWalletMigration, + ): Promise => { + if (migration.balanceMovePaymentTransactionId === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "balanceMovePaymentTransactionId is required before reading fee amount", + ) + } + + if (migration.destinationAmountUsdtMicros === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "destinationAmountUsdtMicros is required before reading fee amount", + ) + } + + if (migration.destinationStartingBalanceUsdtMicros === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "destinationStartingBalanceUsdtMicros is required before reading fee amount", + ) + } + + const currentBalance = await balanceForWallet({ + walletId: migration.destinationUsdtWalletId, + currency: WalletCurrency.Usdt, + }) + if (currentBalance instanceof Error) return currentBalance + if (!isUsdtAmount(currentBalance)) { + return new InvalidCashWalletCutoverAmountError("Expected USDT balance") + } + + return destinationShortfallUsdtMicros({ + targetUsdtMicros: migration.destinationAmountUsdtMicros, + startingUsdtMicros: migration.destinationStartingBalanceUsdtMicros, + currentUsdtMicros: currentBalance.asSmallestUnits(), + }) + }, + }, + treasuryService: { + getTreasuryWalletId: deps.getTreasuryWalletId ?? getFunderWalletId, + }, + pointerService: { + flipDefaultWallet: async ({ + accountId, + destinationWalletId, + }: { + accountId: AccountId + destinationWalletId: WalletId + }): Promise<{ previousDefaultWalletId: WalletId } | ApplicationError> => { + const account = await accountsRepo.findById(accountId) + if (account instanceof Error) return account + + const previousDefaultWalletId = account.defaultWalletId + const updated = await updateDefaultWallet({ + accountId, + walletId: destinationWalletId, + }) + if (updated instanceof Error) return updated + + return { previousDefaultWalletId } + }, + }, + legacyWalletVerifier: { + verifyLegacyWalletZero: async ({ + legacyUsdWalletId, + }: { + legacyUsdWalletId: WalletId + }): Promise => { + const balance = await balanceForWallet({ + walletId: legacyUsdWalletId, + currency: WalletCurrency.Usd, + }) + if (balance instanceof Error) return balance + if (!isUsdAmount(balance) || !balance.isZero()) { + return new CashWalletMigrationFailedError("Legacy USD wallet is not zero") + } + return true + }, + }, + } +} diff --git a/src/app/cash-wallet-cutover/state-machine.ts b/src/app/cash-wallet-cutover/state-machine.ts new file mode 100644 index 000000000..6bd152b8d --- /dev/null +++ b/src/app/cash-wallet-cutover/state-machine.ts @@ -0,0 +1,43 @@ +import { InvalidCashWalletMigrationTransitionError } from "./errors" + +const transitions: Partial< + Record +> = { + not_started: ["started"], + started: ["provisioned", "failed"], + provisioned: ["balance_read", "failed", "skipped_already_migrated"], + balance_read: ["invoice_created", "pointer_flipped", "failed"], + invoice_created: ["balance_move_sending", "failed", "requires_operator_review"], + balance_move_sending: ["balance_move_sent", "failed", "requires_operator_review"], + balance_move_sent: ["balance_move_verified", "failed", "requires_operator_review"], + balance_move_verified: [ + "fee_reimbursement_invoice_created", + "fee_reimbursed", + "failed", + "requires_operator_review", + ], + fee_reimbursement_invoice_created: [ + "fee_reimbursement_sending", + "failed", + "requires_operator_review", + ], + fee_reimbursement_sending: ["fee_reimbursed", "failed", "requires_operator_review"], + fee_reimbursed: ["pointer_flipped", "failed"], + pointer_flipped: ["legacy_zero_verified", "failed"], + legacy_zero_verified: ["complete", "failed"], + rollback_started: ["rolled_back", "failed"], +} + +export const assertCanTransition = ( + from: CashWalletMigrationStatus, + to: CashWalletMigrationStatus, +): true | InvalidCashWalletMigrationTransitionError => { + if (transitions[from]?.includes(to)) return true + return new InvalidCashWalletMigrationTransitionError( + `Invalid migration transition: ${from} -> ${to}`, + ) +} + +export const nextResumeStatus = ( + status: CashWalletMigrationStatus, +): CashWalletMigrationStatus => status diff --git a/src/app/cash-wallet-cutover/worker.ts b/src/app/cash-wallet-cutover/worker.ts new file mode 100644 index 000000000..67b26250f --- /dev/null +++ b/src/app/cash-wallet-cutover/worker.ts @@ -0,0 +1,497 @@ +import { assertCanTransition } from "./state-machine" +import { usdCentsToUsdtMicros, usdtMicrosToUsdCentsCeil } from "./amount-conversion" +import { + InvalidCashWalletCutoverAmountError, + InvalidCashWalletMigrationTransitionError, +} from "./errors" + +type CashWalletMigrationTransitionRepository = { + transitionMigration(args: { + id: string + from: CashWalletMigrationStatus + to: CashWalletMigrationStatus + cutoverVersion: number + runId: string + patch?: Partial + }): Promise +} + +type CashWalletMigrationInvoiceService = { + createInvoice(args: { + recipientWalletId: WalletId + amount: string + memo: string + }): Promise +} + +type CashWalletMigrationNoAmountInvoiceService = { + createNoAmountInvoice(args: { + recipientWalletId: WalletId + memo: string + }): Promise +} + +type CashWalletMigrationPaymentService = { + payInvoice(args: { + senderWalletId: WalletId + paymentRequest: string + senderAmountUsdCents?: string + }): Promise<{ transactionId: IbexTransactionId } | ApplicationError> +} + +type CashWalletMigrationBalanceVerifier = { + verifyBalanceMove(args: { + legacyUsdWalletId: WalletId + destinationUsdtWalletId: WalletId + sourceBalanceUsdCents?: string + destinationAmountUsdtMicros?: string + transactionId: IbexTransactionId + }): Promise +} + +type CashWalletMigrationPointerService = { + flipDefaultWallet(args: { + accountId: AccountId + destinationWalletId: WalletId + }): Promise<{ previousDefaultWalletId: WalletId } | ApplicationError> +} + +type CashWalletMigrationLegacyWalletVerifier = { + verifyLegacyWalletZero(args: { + legacyUsdWalletId: WalletId + }): Promise +} + +type CashWalletMigrationProvisioningService = { + ensureDestinationWallet(args: { + accountId: AccountId + destinationUsdtWalletId: WalletId + }): Promise +} + +export const startCashWalletMigration = async ({ + migration, + migrationsRepo, + startedAt, +}: { + migration: CashWalletMigration + migrationsRepo: CashWalletMigrationTransitionRepository + startedAt: Date +}): Promise => { + const transition = assertCanTransition(migration.status, "started") + if (transition instanceof Error) return transition + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "started", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { startedAt }, + }) +} + +export const provisionCashWalletMigrationDestination = async ({ + migration, + provisioningService, + migrationsRepo, +}: { + migration: CashWalletMigration + provisioningService: CashWalletMigrationProvisioningService + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "provisioned") + if (transition instanceof Error) return transition + + const provisioned = await provisioningService.ensureDestinationWallet({ + accountId: migration.accountId, + destinationUsdtWalletId: migration.destinationUsdtWalletId, + }) + if (provisioned instanceof Error) return provisioned + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "provisioned", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + }) +} + +export const recordCashWalletMigrationBalance = async ({ + migration, + migrationsRepo, + sourceBalanceUsdCents, + destinationStartingBalanceUsdtMicros, +}: { + migration: CashWalletMigration + migrationsRepo: CashWalletMigrationTransitionRepository + sourceBalanceUsdCents: string + destinationStartingBalanceUsdtMicros: string +}): Promise => { + const destinationAmountUsdtMicros = usdCentsToUsdtMicros(sourceBalanceUsdCents) + if (destinationAmountUsdtMicros instanceof Error) return destinationAmountUsdtMicros + + const transition = assertCanTransition(migration.status, "balance_read") + if (transition instanceof Error) return transition + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "balance_read", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { + sourceBalanceUsdCents, + destinationAmountUsdtMicros, + destinationStartingBalanceUsdtMicros, + }, + }) +} + +export const sendCashWalletMigrationBalanceMovePayment = async ({ + migration, + paymentService, + migrationsRepo, +}: { + migration: CashWalletMigration + paymentService: CashWalletMigrationPaymentService + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "balance_move_sending") + if (transition instanceof Error) return transition + + if (migration.balanceMoveInvoicePaymentRequest === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "balanceMoveInvoicePaymentRequest is required before balance move payment sending", + ) + } + + if (migration.sourceBalanceUsdCents === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "sourceBalanceUsdCents is required before balance move payment sending", + ) + } + + const payment = await paymentService.payInvoice({ + senderWalletId: migration.legacyUsdWalletId, + paymentRequest: migration.balanceMoveInvoicePaymentRequest, + senderAmountUsdCents: migration.sourceBalanceUsdCents, + }) + if (payment instanceof Error) return payment + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "balance_move_sending", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { + balanceMovePaymentTransactionId: payment.transactionId, + }, + }) +} + +export const markCashWalletMigrationBalanceMoveSent = async ({ + migration, + migrationsRepo, +}: { + migration: CashWalletMigration + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "balance_move_sent") + if (transition instanceof Error) return transition + + if (migration.balanceMovePaymentTransactionId === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "balanceMovePaymentTransactionId is required before marking balance move sent", + ) + } + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "balance_move_sent", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + }) +} + +export const verifyCashWalletMigrationBalanceMove = async ({ + migration, + balanceVerifier, + migrationsRepo, +}: { + migration: CashWalletMigration + balanceVerifier: CashWalletMigrationBalanceVerifier + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "balance_move_verified") + if (transition instanceof Error) return transition + + if (migration.balanceMovePaymentTransactionId === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "balanceMovePaymentTransactionId is required before verifying balance move", + ) + } + + const verified = await balanceVerifier.verifyBalanceMove({ + legacyUsdWalletId: migration.legacyUsdWalletId, + destinationUsdtWalletId: migration.destinationUsdtWalletId, + sourceBalanceUsdCents: migration.sourceBalanceUsdCents, + destinationAmountUsdtMicros: migration.destinationAmountUsdtMicros, + transactionId: migration.balanceMovePaymentTransactionId as IbexTransactionId, + }) + if (verified instanceof Error) return verified + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "balance_move_verified", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + }) +} + +export const createCashWalletMigrationBalanceMoveInvoice = async ({ + migration, + invoiceService, + migrationsRepo, +}: { + migration: CashWalletMigration + invoiceService: CashWalletMigrationNoAmountInvoiceService + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "invoice_created") + if (transition instanceof Error) return transition + + if (migration.destinationAmountUsdtMicros === undefined) { + return new InvalidCashWalletCutoverAmountError( + "destinationAmountUsdtMicros is required before balance move invoice creation", + ) + } + + const invoice = await invoiceService.createNoAmountInvoice({ + recipientWalletId: migration.destinationUsdtWalletId, + memo: `cash-wallet-cutover:${migration.runId}:${migration.id}:balance-move`, + }) + if (invoice instanceof Error) return invoice + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "invoice_created", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { + balanceMoveInvoicePaymentRequest: invoice.paymentRequest, + balanceMoveInvoicePaymentHash: invoice.paymentHash, + }, + }) +} + +export const createCashWalletMigrationFeeReimbursementInvoice = async ({ + migration, + invoiceService, + migrationsRepo, + feeAmountUsdtMicros, +}: { + migration: CashWalletMigration + invoiceService: CashWalletMigrationInvoiceService + migrationsRepo: CashWalletMigrationTransitionRepository + feeAmountUsdtMicros: string +}): Promise => { + const feeAmountUsdCents = usdtMicrosToUsdCentsCeil(feeAmountUsdtMicros) + if (feeAmountUsdCents instanceof Error) return feeAmountUsdCents + + const transition = assertCanTransition( + migration.status, + "fee_reimbursement_invoice_created", + ) + if (transition instanceof Error) return transition + + const invoice = await invoiceService.createInvoice({ + recipientWalletId: migration.destinationUsdtWalletId, + amount: feeAmountUsdtMicros, + memo: `cash-wallet-cutover:${migration.runId}:${migration.id}:fee-reimbursement`, + }) + if (invoice instanceof Error) return invoice + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "fee_reimbursement_invoice_created", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { + feeAmountUsdCents, + feeAmountUsdtMicros, + feeReimbursementInvoicePaymentRequest: invoice.paymentRequest, + feeReimbursementInvoicePaymentHash: invoice.paymentHash, + }, + }) +} + +export const skipCashWalletMigrationFeeReimbursement = async ({ + migration, + migrationsRepo, +}: { + migration: CashWalletMigration + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "fee_reimbursed") + if (transition instanceof Error) return transition + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "fee_reimbursed", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { + feeAmountUsdCents: "0", + feeAmountUsdtMicros: "0", + }, + }) +} + +export const sendCashWalletMigrationFeeReimbursementPayment = async ({ + migration, + treasuryWalletId, + paymentService, + migrationsRepo, +}: { + migration: CashWalletMigration + treasuryWalletId: WalletId + paymentService: CashWalletMigrationPaymentService + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "fee_reimbursement_sending") + if (transition instanceof Error) return transition + + if (migration.feeReimbursementInvoicePaymentRequest === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "feeReimbursementInvoicePaymentRequest is required before fee reimbursement sending", + ) + } + + const payment = await paymentService.payInvoice({ + senderWalletId: treasuryWalletId, + paymentRequest: migration.feeReimbursementInvoicePaymentRequest, + }) + if (payment instanceof Error) return payment + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "fee_reimbursement_sending", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { + feeReimbursementPaymentTransactionId: payment.transactionId, + }, + }) +} + +export const markCashWalletMigrationFeeReimbursed = async ({ + migration, + migrationsRepo, +}: { + migration: CashWalletMigration + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "fee_reimbursed") + if (transition instanceof Error) return transition + + if (migration.feeReimbursementPaymentTransactionId === undefined) { + return new InvalidCashWalletMigrationTransitionError( + "feeReimbursementPaymentTransactionId is required before marking fee reimbursed", + ) + } + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "fee_reimbursed", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + }) +} + +export const flipCashWalletMigrationDefaultPointer = async ({ + migration, + pointerService, + migrationsRepo, +}: { + migration: CashWalletMigration + pointerService: CashWalletMigrationPointerService + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "pointer_flipped") + if (transition instanceof Error) return transition + + const pointer = await pointerService.flipDefaultWallet({ + accountId: migration.accountId, + destinationWalletId: migration.destinationUsdtWalletId, + }) + if (pointer instanceof Error) return pointer + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "pointer_flipped", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { + previousDefaultWalletId: pointer.previousDefaultWalletId, + }, + }) +} + +export const verifyCashWalletMigrationLegacyZero = async ({ + migration, + legacyWalletVerifier, + migrationsRepo, +}: { + migration: CashWalletMigration + legacyWalletVerifier: CashWalletMigrationLegacyWalletVerifier + migrationsRepo: CashWalletMigrationTransitionRepository +}): Promise => { + const transition = assertCanTransition(migration.status, "legacy_zero_verified") + if (transition instanceof Error) return transition + + const verified = await legacyWalletVerifier.verifyLegacyWalletZero({ + legacyUsdWalletId: migration.legacyUsdWalletId, + }) + if (verified instanceof Error) return verified + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "legacy_zero_verified", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + }) +} + +export const completeCashWalletMigration = async ({ + migration, + migrationsRepo, + completedAt, +}: { + migration: CashWalletMigration + migrationsRepo: CashWalletMigrationTransitionRepository + completedAt: Date +}): Promise => { + const transition = assertCanTransition(migration.status, "complete") + if (transition instanceof Error) return transition + + return migrationsRepo.transitionMigration({ + id: migration.id, + from: migration.status, + to: "complete", + cutoverVersion: migration.cutoverVersion, + runId: migration.runId, + patch: { completedAt }, + }) +} diff --git a/src/app/errors.ts b/src/app/errors.ts index 6e747daf6..4740230f4 100644 --- a/src/app/errors.ts +++ b/src/app/errors.ts @@ -19,6 +19,7 @@ import * as PubSubErrors from "@domain/pubsub/errors" import * as CaptchaErrors from "@domain/captcha/errors" import * as AuthenticationErrors from "@domain/authentication/errors" import * as UserErrors from "@domain/users/errors" +import * as CashWalletCutoverErrors from "@app/cash-wallet-cutover/errors" import * as LedgerFacadeErrors from "@services/ledger/domain/errors" import * as KratosErrors from "@services/kratos/errors" @@ -51,6 +52,7 @@ export const ApplicationErrors = { ...CaptchaErrors, ...AuthenticationErrors, ...UserErrors, + ...CashWalletCutoverErrors, ...KratosErrors, ...LedgerFacadeErrors, diff --git a/src/app/index.ts b/src/app/index.ts index b006e8446..46afb0463 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -14,6 +14,7 @@ import * as WalletsMod from "./wallets" import * as PaymentsMod from "./payments" import * as MerchantsMod from "./merchants" import * as SwapMod from "./swap" +import * as CashWalletCutoverMod from "./cash-wallet-cutover" const allFunctions = { Accounts: { ...AccountsMod }, @@ -30,6 +31,7 @@ const allFunctions = { Payments: { ...PaymentsMod }, Merchants: { ...MerchantsMod }, Swap: { ...SwapMod }, + CashWalletCutover: { ...CashWalletCutoverMod }, } as const let subModule: keyof typeof allFunctions @@ -60,4 +62,5 @@ export const { Payments, Merchants, Swap, + CashWalletCutover, } = allFunctions diff --git a/src/graphql/admin/mutations.ts b/src/graphql/admin/mutations.ts index 070a0d7c7..fcf3f6ce1 100644 --- a/src/graphql/admin/mutations.ts +++ b/src/graphql/admin/mutations.ts @@ -3,6 +3,7 @@ import { GT } from "@graphql/index" import AccountUpdateLevelMutation from "@graphql/admin/root/mutation/account-update-level" import AccountUpdateStatusMutation from "@graphql/admin/root/mutation/account-update-status" import BusinessUpdateMapInfoMutation from "@graphql/admin/root/mutation/business-update-map-info" +import CashWalletCutoverUpdateMutation from "@graphql/admin/root/mutation/cash-wallet-cutover-update" import UserUpdatePhoneMutation from "./root/mutation/user-update-phone" import BusinessDeleteMapInfoMutation from "./root/mutation/delete-business-map" @@ -13,8 +14,7 @@ import MerchantMapDeleteMutation from "./root/mutation/merchant-map-delete" import MerchantMapValidateMutation from "./root/mutation/merchant-map-validate" export const mutationFields = { - unauthed: { - }, + unauthed: {}, authed: { userUpdatePhone: UserUpdatePhoneMutation, accountUpdateLevel: AccountUpdateLevelMutation, @@ -25,6 +25,7 @@ export const mutationFields = { businessDeleteMapInfo: BusinessDeleteMapInfoMutation, sendNotification: SendNotificationMutation, cashoutNotificationSend: sendCashoutSettledNotification, + cashWalletCutoverUpdate: CashWalletCutoverUpdateMutation, }, } diff --git a/src/graphql/admin/queries.ts b/src/graphql/admin/queries.ts index a069657df..deafdfd9f 100644 --- a/src/graphql/admin/queries.ts +++ b/src/graphql/admin/queries.ts @@ -1,4 +1,5 @@ import { GT } from "@graphql/index" +import CashWalletCutoverQuery from "@graphql/shared/root/query/cash-wallet-cutover" import AllLevelsQuery from "./root/query/all-levels" import LightningInvoiceQuery from "./root/query/lightning-invoice" @@ -36,6 +37,7 @@ export const queryFields = { idDocumentReadUrl: IdDocumentReadUrlQuery, notificationTopics: NotificationTopicsQuery, bridgeReconciliationOrphans: BridgeReconciliationOrphansQuery, + cashWalletCutover: CashWalletCutoverQuery, }, } diff --git a/src/graphql/admin/root/mutation/cash-wallet-cutover-update.ts b/src/graphql/admin/root/mutation/cash-wallet-cutover-update.ts new file mode 100644 index 000000000..ff2df55e8 --- /dev/null +++ b/src/graphql/admin/root/mutation/cash-wallet-cutover-update.ts @@ -0,0 +1,61 @@ +import { GT } from "@graphql/index" +import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import CashWalletCutoverPayload from "@graphql/admin/types/payload/cash-wallet-cutover" +import CashWalletCutoverState from "@graphql/shared/types/scalar/cash-wallet-cutover-state" +import Timestamp from "@graphql/shared/types/scalar/timestamp" +import { CashWalletCutoverRepository } from "@services/mongoose/cash-wallet-cutover" + +const CashWalletCutoverUpdateInput = GT.Input({ + name: "CashWalletCutoverUpdateInput", + fields: () => ({ + state: { type: GT.NonNull(CashWalletCutoverState) }, + scheduledAt: { type: Timestamp }, + cutoverVersion: { type: GT.Int }, + runId: { type: GT.String }, + pauseReason: { type: GT.String }, + }), +}) + +const CashWalletCutoverUpdateMutation = GT.Field< + null, + GraphQLAdminContext, + { + input: { + state: CashWalletCutoverState | Error + scheduledAt?: Date | Error + cutoverVersion?: number + runId?: string + pauseReason?: string + } + } +>({ + type: GT.NonNull(CashWalletCutoverPayload), + args: { + input: { type: GT.NonNull(CashWalletCutoverUpdateInput) }, + }, + resolve: async (_, { input }, ctx) => { + if (input.state instanceof Error) { + return { errors: [{ message: input.state.message }] } + } + if (input.scheduledAt instanceof Error) { + return { errors: [{ message: input.scheduledAt.message }] } + } + + const patch: Partial = { + state: input.state, + scheduledAt: input.scheduledAt, + cutoverVersion: input.cutoverVersion, + runId: input.runId, + pauseReason: input.pauseReason, + } + + const result = await CashWalletCutoverRepository().updateConfig(patch, ctx.user.id) + if (result instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(result)] } + } + + return { errors: [], cashWalletCutover: result } + }, +}) + +export default CashWalletCutoverUpdateMutation diff --git a/src/graphql/admin/schema.graphql b/src/graphql/admin/schema.graphql index 87eed84b2..46da132ac 100644 --- a/src/graphql/admin/schema.graphql +++ b/src/graphql/admin/schema.graphql @@ -136,6 +136,38 @@ input BusinessUpdateMapInfoInput { username: Username! } +type CashWalletCutover { + completedAt: Timestamp + cutoverVersion: Int! + pauseReason: String + pausedAt: Timestamp + runId: String + scheduledAt: Timestamp + startedAt: Timestamp + state: CashWalletCutoverState! + updatedAt: Timestamp! + updatedBy: String +} + +type CashWalletCutoverPayload { + cashWalletCutover: CashWalletCutover + errors: [Error!]! +} + +enum CashWalletCutoverState { + COMPLETE + IN_PROGRESS + PRE +} + +input CashWalletCutoverUpdateInput { + cutoverVersion: Int + pauseReason: String + runId: String + scheduledAt: Timestamp + state: CashWalletCutoverState! +} + input CashoutNotificationSendInput { accountId: String! amount: Int! @@ -278,6 +310,7 @@ type Mutation { accountUpdateStatus(input: AccountUpdateStatusInput!): AccountDetailPayload! businessDeleteMapInfo(input: BusinessDeleteMapInfoInput!): AccountDetailPayload! businessUpdateMapInfo(input: BusinessUpdateMapInfoInput!): AccountDetailPayload! + cashWalletCutoverUpdate(input: CashWalletCutoverUpdateInput!): CashWalletCutoverPayload! cashoutNotificationSend(input: CashoutNotificationSendInput!): SuccessPayload! merchantMapDelete(input: MerchantMapDeleteInput!): MerchantPayload! merchantMapValidate(input: MerchantMapValidateInput!): MerchantPayload! @@ -335,6 +368,7 @@ type Query { accountDetailsByUsername(username: Username!): AuditedAccount! allLevels: [AccountLevel!]! bridgeReconciliationOrphans(limit: Int = 50, orphanType: String = null, status: String = null): [BridgeReconciliationOrphan!]! + cashWalletCutover: CashWalletCutover! idDocumentReadUrl( """Storage key of the ID document file""" fileKey: String! diff --git a/src/graphql/admin/types/payload/cash-wallet-cutover.ts b/src/graphql/admin/types/payload/cash-wallet-cutover.ts new file mode 100644 index 000000000..5f209898c --- /dev/null +++ b/src/graphql/admin/types/payload/cash-wallet-cutover.ts @@ -0,0 +1,13 @@ +import { GT } from "@graphql/index" +import IError from "@graphql/shared/types/abstract/error" +import CashWalletCutoverObject from "@graphql/shared/types/object/cash-wallet-cutover" + +const CashWalletCutoverPayload = GT.Object({ + name: "CashWalletCutoverPayload", + fields: () => ({ + errors: { type: GT.NonNullList(IError) }, + cashWalletCutover: { type: CashWalletCutoverObject }, + }), +}) + +export default CashWalletCutoverPayload diff --git a/src/graphql/error-map.ts b/src/graphql/error-map.ts index 9f93e8875..480711d87 100644 --- a/src/graphql/error-map.ts +++ b/src/graphql/error-map.ts @@ -792,6 +792,42 @@ export const mapError = (error: ApplicationError): CustomApolloError => { case "InvalidLnurlError": return new InvalidLnurlError({ message: error.message, logger: baseLogger }) + case "CashWalletCutoverInProgressError": + message = "Cash Wallet cutover is in progress. Please try again shortly." + return new ValidationInternalError({ message, logger: baseLogger }) + + case "CashWalletMigrationFailedError": + message = "Cash Wallet migration needs support review." + return new ValidationInternalError({ message, logger: baseLogger }) + + case "CashWalletCutoverPreflightError": + message = error.message + return new ValidationInternalError({ message, logger: baseLogger }) + + case "CashWalletCutoverTreasuryInsufficientBalanceError": + message = error.message + return new ValidationInternalError({ message, logger: baseLogger }) + + case "CashWalletMissingLegacyUsdWalletError": + message = "Legacy USD Cash Wallet is missing for this account." + return new ValidationInternalError({ message, logger: baseLogger }) + + case "CashWalletMissingUsdtWalletError": + message = "USDT Cash Wallet is missing for this account." + return new ValidationInternalError({ message, logger: baseLogger }) + + case "InvalidCashWalletCutoverAmountError": + message = error.message + return new ValidationInternalError({ message, logger: baseLogger }) + + case "InvalidCashWalletMigrationTransitionError": + message = error.message + return new ValidationInternalError({ message, logger: baseLogger }) + + case "InvalidCashWalletCutoverStateTransitionError": + message = error.message + return new ValidationInternalError({ message, logger: baseLogger }) + case "UnknownCaptchaError": message = `Unknown error occurred (code: ${error.name}${ error.message ? ": " + error.message : "" diff --git a/src/graphql/public/queries.ts b/src/graphql/public/queries.ts index 8f8e7f46f..7a401d97e 100644 --- a/src/graphql/public/queries.ts +++ b/src/graphql/public/queries.ts @@ -1,4 +1,5 @@ import { GT } from "@graphql/index" +import CashWalletCutoverQuery from "@graphql/shared/root/query/cash-wallet-cutover" import MeQuery from "@graphql/public/root/query/me" import GlobalsQuery from "@graphql/public/root/query/globals" @@ -44,6 +45,7 @@ export const queryFields = { npubByUsername: NpubByUserNameQuery, isFlashNpub: IsFlashNpubQuery, supportedBanks: SupportedBanksQuery, + cashWalletCutover: CashWalletCutoverQuery, }, authed: { atAccountLevel: { diff --git a/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts b/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts index 7c5b4dd2a..cf663acc1 100644 --- a/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts +++ b/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts @@ -1,4 +1,5 @@ import { Payments } from "@app" +import { resolveCashWalletMutationWalletIdForAccount } from "@app/cash-wallet-cutover" import { checkedToWalletId } from "@domain/wallets" import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" import { GT } from "@graphql/index" @@ -9,7 +10,6 @@ import WalletId from "@graphql/shared/types/scalar/wallet-id" import dedent from "dedent" import FractionalCentAmount from "@graphql/public/types/scalar/cent-amount-fraction" // import { RequestInit, Response } from 'node-fetch' -import { EmailService } from "@services/email" const IntraLedgerUsdPaymentSendInput = GT.Input({ name: "IntraLedgerUsdPaymentSendInput", @@ -34,7 +34,11 @@ const IntraLedgerUsdPaymentSendMutation = GT.Field { + resolve: async ( + _, + args, + { domainAccount, cashWalletClientCapabilities }: GraphQLPublicContextAuth, + ) => { const { walletId, recipientWalletId, amount, memo } = args.input for (const input of [walletId, recipientWalletId, amount, memo]) { if (input instanceof Error) { @@ -52,11 +56,23 @@ const IntraLedgerUsdPaymentSendMutation = GT.Field({ extensions: { complexity: 120, }, @@ -30,7 +31,7 @@ const LnNoAmountUsdInvoiceFeeProbeMutation = GT.Field({ args: { input: { type: GT.NonNull(LnNoAmountUsdInvoiceFeeProbeInput) }, }, - resolve: async (_, args) => { + resolve: async (_, args, { domainAccount, cashWalletClientCapabilities }) => { const { walletId, paymentRequest, amount } = args.input for (const input of [walletId, paymentRequest, amount]) { @@ -39,6 +40,15 @@ const LnNoAmountUsdInvoiceFeeProbeMutation = GT.Field({ } } + const routedWalletId = await resolveCashWalletMutationWalletIdForAccount({ + account: domainAccount, + walletId, + client: cashWalletClientCapabilities, + }) + if (routedWalletId instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(routedWalletId)] } + } + // FLASH FORK: create IBEX fee estimation instead of Galoy fee estimation // const { result: feeSatAmount, error } = // await Payments.getNoAmountLightningFeeEstimationForUsdWallet({ @@ -49,17 +59,19 @@ const LnNoAmountUsdInvoiceFeeProbeMutation = GT.Field({ // TODO: Move Ibex call to Payments interface const checkedAmount = await usdWalletAmountFromWalletId({ - walletId, + walletId: routedWalletId, amount: amount.toString(), }) if (checkedAmount instanceof Error) { return { errors: [mapAndParseErrorForGqlResponse(checkedAmount)] } } - const resp: IbexFeeEstimation | IbexError = await Ibex.getLnFeeEstimation({ - invoice: paymentRequest as Bolt11, - send: checkedAmount, - }) - if (resp instanceof IbexError) return { errors: [mapAndParseErrorForGqlResponse(resp)] } + const resp: IbexFeeEstimation | IbexError = + await Ibex.getLnFeeEstimation({ + invoice: paymentRequest as Bolt11, + send: checkedAmount, + }) + if (resp instanceof IbexError) + return { errors: [mapAndParseErrorForGqlResponse(resp)] } // if (resp.amount === undefined) return new UnexpectedIbexResponse("Unable to parse fee.") // const feeSatAmount: PaymentAmount = { diff --git a/src/graphql/public/root/mutation/ln-noamount-usd-invoice-payment-send.ts b/src/graphql/public/root/mutation/ln-noamount-usd-invoice-payment-send.ts index e8d8d6227..12298ec34 100644 --- a/src/graphql/public/root/mutation/ln-noamount-usd-invoice-payment-send.ts +++ b/src/graphql/public/root/mutation/ln-noamount-usd-invoice-payment-send.ts @@ -14,6 +14,7 @@ import FractionalCentAmount from "@graphql/public/types/scalar/cent-amount-fract import { PaymentSendStatus } from "@domain/bitcoin/lightning" import { usdWalletAmountFromWalletId } from "@app/wallets" +import { resolveCashWalletMutationWalletIdForAccount } from "@app/cash-wallet-cutover" import Ibex from "@services/ibex/client" import { IbexError } from "@services/ibex/errors" @@ -63,7 +64,7 @@ const LnNoAmountUsdInvoicePaymentSendMutation = GT.Field< args: { input: { type: GT.NonNull(LnNoAmountUsdInvoicePaymentInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, cashWalletClientCapabilities }) => { const { walletId, paymentRequest, amount, memo } = args.input if (walletId instanceof InputValidationError) { @@ -90,8 +91,20 @@ const LnNoAmountUsdInvoicePaymentSendMutation = GT.Field< if (!domainAccount) throw new Error("Authentication required") // eslint-disable-next-line @typescript-eslint/no-explicit-any - const usCents = await usdWalletAmountFromWalletId({ + const routedWalletId = await resolveCashWalletMutationWalletIdForAccount({ + account: domainAccount, walletId, + client: cashWalletClientCapabilities, + }) + if (routedWalletId instanceof Error) { + return { + status: "failed", + errors: [mapAndParseErrorForGqlResponse(routedWalletId)], + } + } + + const usCents = await usdWalletAmountFromWalletId({ + walletId: routedWalletId, amount: amount.toString(), }) if (usCents instanceof Error) { @@ -102,7 +115,7 @@ const LnNoAmountUsdInvoicePaymentSendMutation = GT.Field< } const PayLightningInvoice = await Ibex.payInvoice({ invoice: paymentRequest as Bolt11, - accountId: walletId, + accountId: routedWalletId, send: usCents, }) diff --git a/src/graphql/public/root/mutation/ln-usd-invoice-create.ts b/src/graphql/public/root/mutation/ln-usd-invoice-create.ts index 2cbedab73..064a5d4d3 100644 --- a/src/graphql/public/root/mutation/ln-usd-invoice-create.ts +++ b/src/graphql/public/root/mutation/ln-usd-invoice-create.ts @@ -9,6 +9,7 @@ import WalletId from "@graphql/shared/types/scalar/wallet-id" import LnInvoicePayload from "@graphql/public/types/payload/ln-invoice" import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" import { Wallets } from "@app/index" +import { resolveCashWalletMutationWalletIdForAccount } from "@app/cash-wallet-cutover" import FractionalCentAmount from "@graphql/public/types/scalar/cent-amount-fraction" const LnUsdInvoiceCreateInput = GT.Input({ @@ -18,7 +19,10 @@ const LnUsdInvoiceCreateInput = GT.Input({ type: GT.NonNull(WalletId), description: "Wallet ID for a USD wallet belonging to the current user.", }, - amount: { type: GT.NonNull(FractionalCentAmount), description: "Amount in USD cents." }, + amount: { + type: GT.NonNull(FractionalCentAmount), + description: "Amount in USD cents.", + }, memo: { type: Memo, description: "Optional memo for the lightning invoice." }, expiresIn: { type: Minutes, @@ -27,7 +31,7 @@ const LnUsdInvoiceCreateInput = GT.Input({ }), }) -const LnUsdInvoiceCreateMutation = GT.Field({ +const LnUsdInvoiceCreateMutation = GT.Field({ extensions: { complexity: 120, }, @@ -39,7 +43,7 @@ const LnUsdInvoiceCreateMutation = GT.Field({ args: { input: { type: GT.NonNull(LnUsdInvoiceCreateInput) }, }, - resolve: async (_, args) => { + resolve: async (_, args, { domainAccount, cashWalletClientCapabilities }) => { const { walletId, amount, memo, expiresIn } = args.input for (const input of [walletId, amount, memo, expiresIn]) { @@ -48,8 +52,17 @@ const LnUsdInvoiceCreateMutation = GT.Field({ } } - const invoice = await Wallets.addInvoiceForSelfForUsdWallet({ + const routedWalletId = await resolveCashWalletMutationWalletIdForAccount({ + account: domainAccount, walletId, + client: cashWalletClientCapabilities, + }) + if (routedWalletId instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(routedWalletId)] } + } + + const invoice = await Wallets.addInvoiceForSelfForUsdWallet({ + walletId: routedWalletId, amount, memo, expiresIn, diff --git a/src/graphql/public/root/mutation/ln-usd-invoice-fee-probe.ts b/src/graphql/public/root/mutation/ln-usd-invoice-fee-probe.ts index ccfb2c7bb..c1a06e346 100644 --- a/src/graphql/public/root/mutation/ln-usd-invoice-fee-probe.ts +++ b/src/graphql/public/root/mutation/ln-usd-invoice-fee-probe.ts @@ -3,6 +3,7 @@ import WalletId from "@graphql/shared/types/scalar/wallet-id" import CentAmountPayload from "@graphql/public/types/payload/cent-amount" import LnPaymentRequest from "@graphql/shared/types/scalar/ln-payment-request" import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import { resolveCashWalletMutationWalletIdForAccount } from "@app/cash-wallet-cutover" import { checkedToWalletId } from "@domain/wallets" @@ -53,7 +54,7 @@ const LnUsdInvoiceFeeProbeMutation = GT.Field< args: { input: { type: GT.NonNull(LnUsdInvoiceFeeProbeInput) }, }, - resolve: async (_, args) => { + resolve: async (_, args, { domainAccount, cashWalletClientCapabilities }) => { const { walletId, paymentRequest } = args.input if (walletId instanceof Error) { @@ -68,6 +69,15 @@ const LnUsdInvoiceFeeProbeMutation = GT.Field< if (walletIdChecked instanceof Error) return { errors: [mapAndParseErrorForGqlResponse(walletIdChecked)] } + const routedWalletId = await resolveCashWalletMutationWalletIdForAccount({ + account: domainAccount, + walletId: walletIdChecked, + client: cashWalletClientCapabilities, + }) + if (routedWalletId instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(routedWalletId)] } + } + // FLASH FORK: create IBEX fee estimation instead of Galoy fee estimation // const { result: feeSatAmount, error } = // await Payments.getLightningFeeEstimationForUsdWallet({ @@ -75,7 +85,7 @@ const LnUsdInvoiceFeeProbeMutation = GT.Field< // uncheckedPaymentRequest: paymentRequest, // }) - const wallet = await WalletsRepository().findById(walletIdChecked) + const wallet = await WalletsRepository().findById(routedWalletId) if (wallet instanceof Error) { return { errors: [mapAndParseErrorForGqlResponse(wallet)] } } @@ -84,8 +94,9 @@ const LnUsdInvoiceFeeProbeMutation = GT.Field< invoice: paymentRequest as Bolt11, currency: wallet.currency, }) - if (resp instanceof IbexError) return { errors: [mapAndParseErrorForGqlResponse(resp)] } - + if (resp instanceof IbexError) + return { errors: [mapAndParseErrorForGqlResponse(resp)] } + return { errors: [], invoiceAmount: resp.invoice, diff --git a/src/graphql/public/root/mutation/onchain-usd-payment-send.ts b/src/graphql/public/root/mutation/onchain-usd-payment-send.ts index b45224a6c..f85b02c55 100644 --- a/src/graphql/public/root/mutation/onchain-usd-payment-send.ts +++ b/src/graphql/public/root/mutation/onchain-usd-payment-send.ts @@ -12,6 +12,7 @@ import FractionalCentAmount from "@graphql/public/types/scalar/cent-amount-fract import { PaymentSendStatus } from "@domain/bitcoin/lightning" import { Wallets } from "@app/index" import { usdWalletAmountFromWalletId } from "@app/wallets" +import { resolveCashWalletMutationWalletIdForAccount } from "@app/cash-wallet-cutover" const OnChainUsdPaymentSendInput = GT.Input({ name: "OnChainUsdPaymentSendInput", @@ -47,7 +48,7 @@ const OnChainUsdPaymentSendMutation = GT.Field< args: { input: { type: GT.NonNull(OnChainUsdPaymentSendInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, cashWalletClientCapabilities }) => { const { walletId, address, amount, memo, speed } = args.input if (walletId instanceof Error) { @@ -67,8 +68,20 @@ const OnChainUsdPaymentSendMutation = GT.Field< } if (!domainAccount) throw new Error("Authentication required") - const usdAmount = await usdWalletAmountFromWalletId({ + const routedWalletId = await resolveCashWalletMutationWalletIdForAccount({ + account: domainAccount, walletId, + client: cashWalletClientCapabilities, + }) + if (routedWalletId instanceof Error) { + return { + status: PaymentSendStatus.Failure.value, + errors: [mapAndParseErrorForGqlResponse(routedWalletId)], + } + } + + const usdAmount = await usdWalletAmountFromWalletId({ + walletId: routedWalletId, amount: amount.toString(), }) if (usdAmount instanceof Error) { @@ -77,24 +90,24 @@ const OnChainUsdPaymentSendMutation = GT.Field< errors: [mapAndParseErrorForGqlResponse(usdAmount)], } } - + const result = await Wallets.payOnChainByWalletId({ senderAccount: domainAccount, - senderWalletId: walletId, + senderWalletId: routedWalletId, amount: usdAmount, address, speed, memo, }) if (result instanceof Error) { - return { - status: PaymentSendStatus.Failure.value, - errors: [mapAndParseErrorForGqlResponse(result)] + return { + status: PaymentSendStatus.Failure.value, + errors: [mapAndParseErrorForGqlResponse(result)], } } return { - status: result.status.value, - errors: [] + status: result.status.value, + errors: [], } }, }) diff --git a/src/graphql/public/root/query/account-default-wallet-id.ts b/src/graphql/public/root/query/account-default-wallet-id.ts index 23763edda..f91ebc4a1 100644 --- a/src/graphql/public/root/query/account-default-wallet-id.ts +++ b/src/graphql/public/root/query/account-default-wallet-id.ts @@ -1,10 +1,11 @@ +import { resolveCashWalletPresentationForAccount } from "@app/cash-wallet-cutover" import { mapError } from "@graphql/error-map" import { GT } from "@graphql/index" import Username from "@graphql/shared/types/scalar/username" import WalletId from "@graphql/shared/types/scalar/wallet-id" import { AccountsRepository } from "@services/mongoose" -const AccountDefaultWalletIdQuery = GT.Field({ +const AccountDefaultWalletIdQuery = GT.Field({ deprecationReason: "will be migrated to AccountDefaultWalletId", type: GT.NonNull(WalletId), args: { @@ -12,7 +13,7 @@ const AccountDefaultWalletIdQuery = GT.Field({ type: GT.NonNull(Username), }, }, - resolve: async (_, args) => { + resolve: async (_, args, { cashWalletClientCapabilities }) => { const { username } = args if (username instanceof Error) { @@ -24,8 +25,13 @@ const AccountDefaultWalletIdQuery = GT.Field({ throw mapError(account) } - const walletId = account.defaultWalletId - return walletId + const presentation = await resolveCashWalletPresentationForAccount({ + account, + client: cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) + + return presentation.defaultWalletId }, }) diff --git a/src/graphql/public/root/query/account-default-wallet.ts b/src/graphql/public/root/query/account-default-wallet.ts index 1387c0169..8d5b12f47 100644 --- a/src/graphql/public/root/query/account-default-wallet.ts +++ b/src/graphql/public/root/query/account-default-wallet.ts @@ -1,4 +1,4 @@ -import { Wallets } from "@app" +import { resolveCashWalletPresentationForAccount } from "@app/cash-wallet-cutover" import { CouldNotFindWalletFromUsernameAndCurrencyError } from "@domain/errors" import { mapError } from "@graphql/error-map" import { GT } from "@graphql/index" @@ -7,7 +7,7 @@ import WalletCurrency from "@graphql/shared/types/scalar/wallet-currency" import PublicWallet from "@graphql/public/types/abstract/public-wallet" import { AccountsRepository } from "@services/mongoose" -const AccountDefaultWalletQuery = GT.Field({ +const AccountDefaultWalletQuery = GT.Field({ type: GT.NonNull(PublicWallet), args: { username: { @@ -15,7 +15,7 @@ const AccountDefaultWalletQuery = GT.Field({ }, walletCurrency: { type: WalletCurrency }, }, - resolve: async (_, args) => { + resolve: async (_, args, { cashWalletClientCapabilities }) => { const { username, walletCurrency } = args if (username instanceof Error) { @@ -27,16 +27,21 @@ const AccountDefaultWalletQuery = GT.Field({ throw mapError(account) } - const wallets = await Wallets.listWalletsByAccountId(account.id) - if (wallets instanceof Error) { - throw mapError(wallets) - } + const presentation = await resolveCashWalletPresentationForAccount({ + account, + client: cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) if (!walletCurrency) { - return wallets.find((wallet) => wallet.id === account.defaultWalletId) + return presentation.wallets.find( + (wallet) => wallet.id === presentation.defaultWalletId, + ) } - const wallet = wallets.find((wallet) => wallet.currency === walletCurrency) + const wallet = presentation.wallets.find( + (wallet) => wallet.currency === walletCurrency, + ) if (!wallet) { throw mapError(new CouldNotFindWalletFromUsernameAndCurrencyError(username)) } diff --git a/src/graphql/public/schema.graphql b/src/graphql/public/schema.graphql index e0bac3237..30c15eada 100644 --- a/src/graphql/public/schema.graphql +++ b/src/graphql/public/schema.graphql @@ -358,6 +358,25 @@ input CaptchaRequestAuthCodeInput { validationCode: String! } +type CashWalletCutover { + completedAt: Timestamp + cutoverVersion: Int! + pauseReason: String + pausedAt: Timestamp + runId: String + scheduledAt: Timestamp + startedAt: Timestamp + state: CashWalletCutoverState! + updatedAt: Timestamp! + updatedBy: String +} + +enum CashWalletCutoverState { + COMPLETE + IN_PROGRESS + PRE +} + type CashoutOffer { """The rate used when withdrawing to a JMD bank account""" exchangeRate: JMDCents @@ -1296,6 +1315,7 @@ type Query { btcPrice(currency: DisplayCurrency! = "USD"): Price @deprecated(reason: "Deprecated in favor of realtimePrice") btcPriceList(range: PriceGraphRange!): [PricePoint] businessMapMarkers: [MapMarker!]! + cashWalletCutover: CashWalletCutover! currencyList: [Currency!]! globals: Globals isFlashNpub(input: IsFlashNpubInput!): IsFlashNpubPayload diff --git a/src/graphql/public/types/object/business-account.ts b/src/graphql/public/types/object/business-account.ts index 97daf3670..a9980a89e 100644 --- a/src/graphql/public/types/object/business-account.ts +++ b/src/graphql/public/types/object/business-account.ts @@ -1,4 +1,8 @@ -import { Accounts, Prices, Wallets } from "@app" +import { Accounts, Prices } from "@app" +import { + cashWalletTransactionWalletIdsForPresentation, + resolveCashWalletPresentationForAccount, +} from "@app/cash-wallet-cutover" import { majorToMinorUnit, @@ -15,8 +19,6 @@ import { checkedConnectionArgs, } from "@graphql/connections" -import { WalletsRepository } from "@services/mongoose" - import IAccount from "../abstract/account" import Wallet from "../../../shared/types/abstract/wallet" @@ -30,7 +32,7 @@ import { TransactionConnection } from "../../../shared/types/object/transaction" import RealtimePrice from "./realtime-price" import { NotificationSettings } from "./notification-settings" -const BusinessAccount = GT.Object({ +const BusinessAccount = GT.Object({ name: "BusinessAccount", interfaces: () => [IAccount], isTypeOf: () => false, @@ -42,15 +44,36 @@ const BusinessAccount = GT.Object({ wallets: { type: GT.NonNullList(Wallet), - resolve: async (source: Account) => { - return Wallets.listWalletsByAccountId(source.id) + resolve: async ( + source: Account, + args, + { cashWalletClientCapabilities }: GraphQLPublicContextAuth, + ) => { + const presentation = await resolveCashWalletPresentationForAccount({ + account: source, + client: cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) + + return presentation.wallets }, }, defaultWalletId: { type: GT.NonNull(WalletId), - resolve: (source, args, { domainAccount }: { domainAccount: Account }) => - domainAccount.defaultWalletId, + resolve: async ( + source: Account, + args, + { cashWalletClientCapabilities }: GraphQLPublicContextAuth, + ) => { + const presentation = await resolveCashWalletPresentationForAccount({ + account: source, + client: cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) + + return presentation.defaultWalletId + }, }, level: { @@ -60,8 +83,7 @@ const BusinessAccount = GT.Object({ displayCurrency: { type: GT.NonNull(DisplayCurrency), - resolve: (source, args, { domainAccount }: { domainAccount: Account }) => - domainAccount.displayCurrency, + resolve: (source, args, { domainAccount }) => domainAccount.displayCurrency, }, realtimePrice: { @@ -123,21 +145,28 @@ const BusinessAccount = GT.Object({ type: GT.List(WalletId), }, }, - resolve: async (source, args) => { + resolve: async ( + source: Account, + args, + { cashWalletClientCapabilities }: GraphQLPublicContextAuth, + ) => { const paginationArgs = checkedConnectionArgs(args) if (paginationArgs instanceof Error) { throw paginationArgs } + const presentation = await resolveCashWalletPresentationForAccount({ + account: source, + client: cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) + let { walletIds } = args - if (!walletIds) { - const wallets = await WalletsRepository().listByAccountId(source.id) - if (wallets instanceof Error) { - throw mapError(wallets) - } - walletIds = wallets.map((wallet) => wallet.id) - } + walletIds = cashWalletTransactionWalletIdsForPresentation({ + walletIds, + presentation, + }) const { result, error } = await Accounts.getTransactionsForAccountByWalletIds({ account: source, diff --git a/src/graphql/public/types/object/consumer-account.ts b/src/graphql/public/types/object/consumer-account.ts index 53e3e5a17..660b3a999 100644 --- a/src/graphql/public/types/object/consumer-account.ts +++ b/src/graphql/public/types/object/consumer-account.ts @@ -1,4 +1,8 @@ -import { Accounts, Prices, Wallets } from "@app" +import { Accounts, Prices } from "@app" +import { + cashWalletTransactionWalletIdsForPresentation, + resolveCashWalletPresentationForAccount, +} from "@app/cash-wallet-cutover" import { majorToMinorUnit, @@ -21,8 +25,6 @@ import WalletId from "@graphql/shared/types/scalar/wallet-id" import RealtimePrice from "@graphql/public/types/object/realtime-price" import DisplayCurrency from "@graphql/shared/types/scalar/display-currency" -import { WalletsRepository } from "@services/mongoose" - import { listEndpoints } from "@app/callback" import AccountLevel from "../../../shared/types/scalar/account-level" @@ -54,14 +56,28 @@ const ConsumerAccount = GT.Object({ wallets: { type: GT.NonNullList(Wallet), - resolve: async (source) => { - return Wallets.listWalletsByAccountId(source.id) + resolve: async (source, args, { cashWalletClientCapabilities }) => { + const presentation = await resolveCashWalletPresentationForAccount({ + account: source, + client: cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) + + return presentation.wallets }, }, defaultWalletId: { type: GT.NonNull(WalletId), - resolve: (source) => source.defaultWalletId, + resolve: async (source, args, { cashWalletClientCapabilities }) => { + const presentation = await resolveCashWalletPresentationForAccount({ + account: source, + client: cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) + + return presentation.defaultWalletId + }, }, displayCurrency: { @@ -145,21 +161,24 @@ const ConsumerAccount = GT.Object({ type: GT.List(WalletId), }, }, - resolve: async (source, args) => { + resolve: async (source, args, { cashWalletClientCapabilities }) => { const paginationArgs = checkedConnectionArgs(args) if (paginationArgs instanceof Error) { throw paginationArgs } + const presentation = await resolveCashWalletPresentationForAccount({ + account: source, + client: cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) + let { walletIds } = args - if (!walletIds) { - const wallets = await WalletsRepository().listByAccountId(source.id) - if (wallets instanceof Error) { - throw mapError(wallets) - } - walletIds = wallets.map((wallet) => wallet.id) - } + walletIds = cashWalletTransactionWalletIdsForPresentation({ + walletIds, + presentation, + }) const { result, error } = await Accounts.getTransactionsForAccountByWalletIds({ account: source, diff --git a/src/graphql/shared/root/query/cash-wallet-cutover.ts b/src/graphql/shared/root/query/cash-wallet-cutover.ts new file mode 100644 index 000000000..71ca79e82 --- /dev/null +++ b/src/graphql/shared/root/query/cash-wallet-cutover.ts @@ -0,0 +1,14 @@ +import { GT } from "@graphql/index" +import CashWalletCutoverObject from "@graphql/shared/types/object/cash-wallet-cutover" +import { CashWalletCutoverRepository } from "@services/mongoose/cash-wallet-cutover" + +const CashWalletCutoverQuery = GT.Field({ + type: GT.NonNull(CashWalletCutoverObject), + resolve: async () => { + const config = await CashWalletCutoverRepository().getConfig() + if (config instanceof Error) throw config + return config + }, +}) + +export default CashWalletCutoverQuery diff --git a/src/graphql/shared/types/object/cash-wallet-cutover.ts b/src/graphql/shared/types/object/cash-wallet-cutover.ts new file mode 100644 index 000000000..6570e1b69 --- /dev/null +++ b/src/graphql/shared/types/object/cash-wallet-cutover.ts @@ -0,0 +1,21 @@ +import { GT } from "@graphql/index" +import Timestamp from "@graphql/shared/types/scalar/timestamp" +import CashWalletCutoverState from "@graphql/shared/types/scalar/cash-wallet-cutover-state" + +const CashWalletCutoverObject = GT.Object({ + name: "CashWalletCutover", + fields: () => ({ + state: { type: GT.NonNull(CashWalletCutoverState) }, + scheduledAt: { type: Timestamp }, + startedAt: { type: Timestamp }, + completedAt: { type: Timestamp }, + pausedAt: { type: Timestamp }, + pauseReason: { type: GT.String }, + cutoverVersion: { type: GT.NonNull(GT.Int) }, + runId: { type: GT.String }, + updatedBy: { type: GT.String }, + updatedAt: { type: GT.NonNull(Timestamp) }, + }), +}) + +export default CashWalletCutoverObject diff --git a/src/graphql/shared/types/object/usd-wallet.ts b/src/graphql/shared/types/object/usd-wallet.ts index 27096a000..496527e95 100644 --- a/src/graphql/shared/types/object/usd-wallet.ts +++ b/src/graphql/shared/types/object/usd-wallet.ts @@ -9,6 +9,7 @@ import { mapError } from "@graphql/error-map" import FractionalCentAmount from "@graphql/public/types/scalar/cent-amount-fraction" import { Wallets } from "@app" +import { resolveCashWalletPresentationForAccount } from "@app/cash-wallet-cutover" import { WalletCurrency as WalletCurrencyDomain, USDTAmount } from "@domain/shared" import { WalletType } from "@domain/wallets" @@ -22,6 +23,15 @@ import Lnurl from "../scalar/lnurl" import { TransactionConnection } from "./transaction" +export const usdtMicrosToUsdCents = (usdtMicros: bigint | number | string): number => { + const [wholeMicros, fractionalMicros] = usdtMicros.toString().split(".") + if (fractionalMicros && !/^0+$/.test(fractionalMicros)) { + throw new Error(`Cannot convert fractional USDT micros ${usdtMicros} to USD cents`) + } + + return Number(BigInt(wholeMicros) / 10_000n) +} + const UsdWallet = GT.Object({ name: "UsdWallet", description: @@ -49,17 +59,34 @@ const UsdWallet = GT.Object({ }, balance: { type: FractionalCentAmount, - resolve: async (source) => { + resolve: async (source, args, ctx) => { if (source.type === WalletType.External) return null + let balanceWallet = source + + if ( + "cashWalletClientCapabilities" in ctx && + ctx.domainAccount?.id === source.accountId + ) { + const presentation = await resolveCashWalletPresentationForAccount({ + account: ctx.domainAccount, + client: ctx.cashWalletClientCapabilities, + }) + if (presentation instanceof Error) throw mapError(presentation) + + if (source.id === presentation.legacyUsdWallet?.id) { + balanceWallet = presentation.activeSettlementWallet + } + } + const balance = await Wallets.getBalanceForWallet({ - walletId: source.id, - currency: source.currency, + walletId: balanceWallet.id, + currency: balanceWallet.currency, }) if (balance instanceof Error) { throw mapError(balance) } if (balance instanceof USDTAmount) { - return Number(balance.asSmallestUnits(8)) + return usdtMicrosToUsdCents(balance.asSmallestUnits()) } return Number(balance.asCents(8)) }, diff --git a/src/graphql/shared/types/scalar/cash-wallet-cutover-state.ts b/src/graphql/shared/types/scalar/cash-wallet-cutover-state.ts new file mode 100644 index 000000000..831a377f8 --- /dev/null +++ b/src/graphql/shared/types/scalar/cash-wallet-cutover-state.ts @@ -0,0 +1,12 @@ +import { GT } from "@graphql/index" + +const CashWalletCutoverState = GT.Enum({ + name: "CashWalletCutoverState", + values: { + PRE: { value: "pre" }, + IN_PROGRESS: { value: "in_progress" }, + COMPLETE: { value: "complete" }, + }, +}) + +export default CashWalletCutoverState diff --git a/src/scripts/cash-wallet-cutover.ts b/src/scripts/cash-wallet-cutover.ts new file mode 100644 index 000000000..3bd9a173a --- /dev/null +++ b/src/scripts/cash-wallet-cutover.ts @@ -0,0 +1,130 @@ +#!/usr/bin/env node + +import yargs from "yargs" +import { hideBin } from "yargs/helpers" + +import { CashWalletCutover } from "@app" +import { setupMongoConnection } from "@services/mongodb" +import { + AccountsRepository, + CashWalletCutoverRepository, + WalletsRepository, +} from "@services/mongoose" +import { baseLogger } from "@services/logger" + +const args = yargs(hideBin(process.argv)) + .command("preview", "discover accounts and print the migration plan without writes") + .command("prepare", "discover accounts and upsert migration records") + .command("start", "mark a prepared cutover run in progress") + .command("run-batch", "run one locked migration worker batch") + .command("status", "print cutover config and migration counts") + .command("complete", "mark cutover complete after all migrations finish") + .demandCommand(1) + .option("cutover-version", { type: "number", demandOption: true }) + .option("run-id", { type: "string", demandOption: true }) + .option("operator", { type: "string", default: "unknown" }) + .option("worker-id", { type: "string", default: `worker-${process.pid}` }) + .option("limit", { type: "number", default: 25 }) + .option("lock-stale-seconds", { type: "number", default: 300 }) + .option("configPath", { type: "string", demandOption: true }) + .parseSync() + +const repository = CashWalletCutoverRepository() + +const toJson = (result: unknown) => { + console.log(JSON.stringify(result, null, 2)) +} + +const run = async () => { + const command = args._[0] + const cutoverVersion = args["cutover-version"] + const runId = args["run-id"] + + switch (command) { + case "preview": { + const result = await CashWalletCutover.previewPrimaryCashWalletCutover({ + cutoverVersion, + runId, + }) + if (result instanceof Error) throw result + toJson(result) + return + } + + case "prepare": { + const result = await CashWalletCutover.preparePrimaryCashWalletCutover({ + cutoverVersion, + runId, + accountsRepo: AccountsRepository(), + walletsRepo: WalletsRepository(), + migrationsRepo: repository, + }) + if (result instanceof Error) throw result + toJson(result) + return + } + + case "start": { + const result = await CashWalletCutover.startPrimaryCashWalletCutover({ + cutoverVersion, + runId, + actor: args.operator, + migrationsRepo: repository, + }) + if (result instanceof Error) throw result + toJson(result) + return + } + + case "run-batch": { + const result = await CashWalletCutover.runPrimaryCashWalletCutoverBatch({ + cutoverVersion, + runId, + workerId: args["worker-id"], + limit: args.limit, + lockStaleBefore: new Date(Date.now() - args["lock-stale-seconds"] * 1000), + migrationsRepo: repository, + }) + if (result instanceof Error) throw result + toJson(result) + return + } + + case "status": { + const result = await CashWalletCutover.getPrimaryCashWalletCutoverStatus({ + cutoverVersion, + runId, + migrationsRepo: repository, + }) + if (result instanceof Error) throw result + toJson(result) + return + } + + case "complete": { + const result = await CashWalletCutover.completePrimaryCashWalletCutover({ + cutoverVersion, + runId, + actor: args.operator, + migrationsRepo: repository, + }) + if (result instanceof Error) throw result + toJson(result) + return + } + + default: + throw new Error(`Unsupported cash wallet cutover command: ${command}`) + } +} + +setupMongoConnection() + .then(async (mongoose) => { + await run() + await mongoose?.connection.close() + process.exit(0) + }) + .catch((error) => { + baseLogger.error({ error }, "Cash wallet cutover operator command failed") + process.exit(1) + }) diff --git a/src/servers/graphql-main-server.ts b/src/servers/graphql-main-server.ts index a0c3b1598..dfbeb53e6 100644 --- a/src/servers/graphql-main-server.ts +++ b/src/servers/graphql-main-server.ts @@ -6,7 +6,6 @@ import { AuthorizationError } from "@graphql/error" import { gqlMainSchema, mutationFields, queryFields } from "@graphql/public" import { bootstrap } from "@app/bootstrap" -import { activateLndHealthCheck } from "@services/lnd/health" import { baseLogger } from "@services/logger" import { setupMongoConnection } from "@services/mongodb" import { shield } from "graphql-shield" @@ -20,6 +19,7 @@ import { import { NextFunction, Request, Response } from "express" import { parseIps } from "@domain/accounts-ips" +import { parseCashWalletClientCapabilities } from "@app/cash-wallet-cutover/client-capability" import { startApolloServerForAdminSchema } from "./graphql-admin-server" import { isAuthenticated, startApolloServer } from "./graphql-server" @@ -44,8 +44,12 @@ const setGqlContext = async ( tokenPayload, ip, }) + const cashWalletClientCapabilities = parseCashWalletClientCapabilities(req.headers) - req.gqlContext = gqlContext + req.gqlContext = { + ...gqlContext, + cashWalletClientCapabilities, + } return addAttributesToCurrentSpanAndPropagate( { @@ -56,6 +60,11 @@ const setGqlContext = async ( [SemanticAttributes.HTTP_USER_AGENT]: req.headers["user-agent"], [ACCOUNT_USERNAME]: gqlContext?.domainAccount?.username, [SemanticAttributes.ENDUSER_ID]: tokenPayload?.sub, + "cash_wallet.client_presentation": + cashWalletClientCapabilities.cashWalletPresentation, + "cash_wallet.client_usdt_supported": String( + cashWalletClientCapabilities.hasUsdtCashWalletSupport, + ), }, next, ) @@ -101,9 +110,9 @@ export async function startApolloServerForCoreSchema() { if (require.main === module) { setupMongoConnection(true) .then(async () => { - // activateLndHealthCheck() // + // activateLndHealthCheck() - const res = await bootstrap() + await bootstrap() // if (res instanceof Error) throw res await Promise.race([ diff --git a/src/servers/index.files.d.ts b/src/servers/index.files.d.ts index 29513578f..c918274f8 100644 --- a/src/servers/index.files.d.ts +++ b/src/servers/index.files.d.ts @@ -13,6 +13,7 @@ type GraphQLPublicContext = { domainAccount: Account | undefined ip: IpAddress | undefined sessionId: SessionId | undefined + cashWalletClientCapabilities: import("@app/cash-wallet-cutover/client-capability").CashWalletClientCapabilities } type GraphQLPublicContextAuth = Omit & { diff --git a/src/servers/middlewares/session.ts b/src/servers/middlewares/session.ts index 03201a40a..2631ce286 100644 --- a/src/servers/middlewares/session.ts +++ b/src/servers/middlewares/session.ts @@ -1,6 +1,7 @@ import DataLoader from "dataloader" import { Accounts, Transactions } from "@app" +import { DEFAULT_CASH_WALLET_CLIENT_CAPABILITIES } from "@app/cash-wallet-cutover" import { recordExceptionInCurrentSpan } from "@services/tracing" import jsonwebtoken from "jsonwebtoken" @@ -67,8 +68,7 @@ export const sessionPublicContext = async ({ error: txnMetadata, }) return keys.map(() => undefined) - } - else if (txnMetadata instanceof Error) { + } else if (txnMetadata instanceof Error) { recordExceptionInCurrentSpan({ error: txnMetadata, level: txnMetadata.level, @@ -88,5 +88,6 @@ export const sessionPublicContext = async ({ domainAccount, ip, sessionId, + cashWalletClientCapabilities: DEFAULT_CASH_WALLET_CLIENT_CAPABILITIES, } } diff --git a/src/servers/ws-server.ts b/src/servers/ws-server.ts index 9255a7f46..c98cbb28a 100644 --- a/src/servers/ws-server.ts +++ b/src/servers/ws-server.ts @@ -10,6 +10,7 @@ import jsonwebtoken from "jsonwebtoken" import { parseIps } from "@domain/accounts-ips" import { ErrorLevel } from "@domain/shared" +import { parseCashWalletClientCapabilities } from "@app/cash-wallet-cutover" import jwksRsa from "jwks-rsa" @@ -60,12 +61,16 @@ const getContext = async ( fnName: "getContext", fn: async () => { const connectionParams = ctx.connectionParams + const cashWalletClientCapabilities = parseCashWalletClientCapabilities({ + ...ctx.extra?.request?.headers, + ...connectionParams, + }) // TODO: check if nginx pass the ip to the header // TODO: ip not been used currently for subscription. // implement some rate limiting. const ipString = UNSECURE_IP_FROM_REQUEST_OBJECT - ? connectionParams?.ip ?? ctx.extra?.request?.socket?.remoteAddress + ? (connectionParams?.ip ?? ctx.extra?.request?.socket?.remoteAddress) : connectionParams?.["x-real-ip"] || connectionParams?.["x-forwarded-for"] const ip = parseIps(ipString) @@ -82,10 +87,14 @@ const getContext = async ( sub: kratosCookieRes.kratosUserId, } - return sessionPublicContext({ + const context = await sessionPublicContext({ tokenPayload, ip, }) + return { + ...context, + cashWalletClientCapabilities, + } } const kratosToken = authz?.slice(7) as AuthToken @@ -106,10 +115,14 @@ const getContext = async ( return false } - return sessionPublicContext({ + const context = await sessionPublicContext({ tokenPayload, ip, }) + return { + ...context, + cashWalletClientCapabilities, + } }, })() } diff --git a/src/services/mongoose/cash-wallet-cutover.ts b/src/services/mongoose/cash-wallet-cutover.ts new file mode 100644 index 000000000..7db76ae4b --- /dev/null +++ b/src/services/mongoose/cash-wallet-cutover.ts @@ -0,0 +1,329 @@ +import { randomUUID } from "crypto" + +import { CouldNotUpdateError } from "@domain/errors" + +import { parseRepositoryError } from "./utils" +import { CashWalletCutoverConfig, CashWalletMigration } from "./schema" + +const CONFIG_ID = "cash_wallet_cutover" + +const TERMINAL_STATUSES: CashWalletMigrationStatus[] = [ + "complete", + "failed", + "requires_operator_review", + "skipped_already_migrated", + "rolled_back", +] + +type UpsertMigrationArgs = { + accountId: AccountId + accountUuid?: AccountUuid + legacyUsdWalletId: WalletId + destinationUsdtWalletId: WalletId + previousDefaultWalletId?: WalletId + cutoverVersion: number + runId: string + idempotencyKey: string +} + +type TransitionMigrationArgs = { + id: string + from: CashWalletMigrationStatus + to: CashWalletMigrationStatus + cutoverVersion: number + runId: string + patch?: Partial +} + +type LockMigrationArgs = { + id: string + workerId: string + staleBefore: Date + cutoverVersion: number + runId: string +} + +type MarkMigrationFailedArgs = Omit & { + error: Error + status: "failed" | "requires_operator_review" +} + +const defaultConfig = (): CashWalletCutoverConfig => ({ + state: "pre", + cutoverVersion: 1, + updatedAt: new Date(0), +}) + +const resultToConfig = ( + record: CashWalletCutoverConfigRecord, +): CashWalletCutoverConfig => ({ + state: record.state, + scheduledAt: record.scheduledAt, + startedAt: record.startedAt, + completedAt: record.completedAt, + pausedAt: record.pausedAt, + pauseReason: record.pauseReason, + updatedBy: record.updatedBy, + cutoverVersion: record.cutoverVersion, + runId: record.runId, + updatedAt: record.updatedAt, +}) + +const resultToMigration = (record: CashWalletMigrationRecord): CashWalletMigration => ({ + id: record._id, + accountId: record.accountId as AccountId, + accountUuid: record.accountUuid as AccountUuid | undefined, + legacyUsdWalletId: record.legacyUsdWalletId as WalletId, + destinationUsdtWalletId: record.destinationUsdtWalletId as WalletId, + previousDefaultWalletId: record.previousDefaultWalletId as WalletId | undefined, + cutoverVersion: record.cutoverVersion, + runId: record.runId, + status: record.status, + sourceBalanceUsdCents: record.sourceBalanceUsdCents, + destinationAmountUsdtMicros: record.destinationAmountUsdtMicros, + destinationStartingBalanceUsdtMicros: record.destinationStartingBalanceUsdtMicros, + feeAmountUsdCents: record.feeAmountUsdCents, + feeAmountUsdtMicros: record.feeAmountUsdtMicros, + balanceMoveInvoicePaymentRequest: record.balanceMoveInvoicePaymentRequest, + balanceMoveInvoicePaymentHash: record.balanceMoveInvoicePaymentHash, + balanceMovePaymentTransactionId: record.balanceMovePaymentTransactionId, + feeReimbursementInvoicePaymentRequest: record.feeReimbursementInvoicePaymentRequest, + feeReimbursementInvoicePaymentHash: record.feeReimbursementInvoicePaymentHash, + feeReimbursementPaymentTransactionId: record.feeReimbursementPaymentTransactionId, + estimatedFee: record.estimatedFee, + idempotencyKey: record.idempotencyKey, + attempts: record.attempts, + lastError: record.lastError, + lockedAt: record.lockedAt, + lockedBy: record.lockedBy, + startedAt: record.startedAt, + completedAt: record.completedAt, + updatedAt: record.updatedAt, +}) + +export const CashWalletCutoverRepository = () => { + const getConfig = async (): Promise => { + try { + const result = await CashWalletCutoverConfig.findById(CONFIG_ID) + if (!result) return defaultConfig() + return resultToConfig(result) + } catch (err) { + return parseRepositoryError(err) + } + } + + const updateConfig = async ( + patch: Partial, + actor?: string, + ): Promise => { + try { + const result = await CashWalletCutoverConfig.findOneAndUpdate( + { _id: CONFIG_ID }, + { $set: { ...patch, updatedBy: actor, updatedAt: new Date() } }, + { upsert: true, new: true }, + ) + return resultToConfig(result) + } catch (err) { + return parseRepositoryError(err) + } + } + + const upsertMigration = async ( + args: UpsertMigrationArgs, + ): Promise => { + try { + const now = new Date() + const result = await CashWalletMigration.findOneAndUpdate( + { accountId: args.accountId, runId: args.runId }, + { + $setOnInsert: { + _id: randomUUID(), + ...args, + status: "not_started", + attempts: 0, + updatedAt: now, + }, + }, + { upsert: true, new: true }, + ) + return resultToMigration(result) + } catch (err) { + return parseRepositoryError(err) + } + } + + const findMigrationByAccountId = async ({ + accountId, + cutoverVersion, + runId, + }: { + accountId: AccountId + cutoverVersion: number + runId: string + }): Promise => { + try { + const result = await CashWalletMigration.findOne({ + accountId, + cutoverVersion, + runId, + }) + if (!result) return null + return resultToMigration(result) + } catch (err) { + return parseRepositoryError(err) + } + } + + const transitionMigration = async ({ + id, + from, + to, + cutoverVersion, + runId, + patch = {}, + }: TransitionMigrationArgs): Promise => { + try { + const result = await CashWalletMigration.findOneAndUpdate( + { _id: id, status: from, cutoverVersion, runId }, + { $set: { ...patch, status: to, updatedAt: new Date() } }, + { new: true }, + ) + if (!result) + return new CouldNotUpdateError("Could not transition cash wallet migration") + return resultToMigration(result) + } catch (err) { + return parseRepositoryError(err) + } + } + + const acquireMigrationLock = async ({ + id, + workerId, + staleBefore, + cutoverVersion, + runId, + }: LockMigrationArgs): Promise => { + try { + const result = await CashWalletMigration.findOneAndUpdate( + { + _id: id, + cutoverVersion, + runId, + $or: [{ lockedAt: null }, { lockedAt: { $lt: staleBefore } }], + }, + { $set: { lockedAt: new Date(), lockedBy: workerId, updatedAt: new Date() } }, + { new: true }, + ) + if (!result) + return new CouldNotUpdateError("Could not acquire cash wallet migration lock") + return resultToMigration(result) + } catch (err) { + return parseRepositoryError(err) + } + } + + const releaseMigrationLock = async ({ + id, + workerId, + cutoverVersion, + runId, + }: Omit): Promise< + CashWalletMigration | RepositoryError + > => { + try { + const result = await CashWalletMigration.findOneAndUpdate( + { _id: id, lockedBy: workerId, cutoverVersion, runId }, + { $set: { lockedAt: null, lockedBy: null, updatedAt: new Date() } }, + { new: true }, + ) + if (!result) + return new CouldNotUpdateError("Could not release cash wallet migration lock") + return resultToMigration(result) + } catch (err) { + return parseRepositoryError(err) + } + } + + const markMigrationFailed = async ({ + id, + workerId, + cutoverVersion, + runId, + error, + status, + }: MarkMigrationFailedArgs): Promise => { + try { + const result = await CashWalletMigration.findOneAndUpdate( + { _id: id, lockedBy: workerId, cutoverVersion, runId }, + { + $set: { + status, + lastError: error.message, + lockedAt: null, + lockedBy: null, + updatedAt: new Date(), + }, + $inc: { attempts: 1 }, + }, + { new: true }, + ) + if (!result) + return new CouldNotUpdateError("Could not mark cash wallet migration failed") + return resultToMigration(result) + } catch (err) { + return parseRepositoryError(err) + } + } + + const listRunnableMigrations = async ({ + cutoverVersion, + runId, + limit, + }: { + cutoverVersion: number + runId: string + limit?: number + }): Promise => { + try { + const results = await CashWalletMigration.find({ + cutoverVersion, + runId, + status: { $nin: TERMINAL_STATUSES }, + }) + .sort({ updatedAt: 1 }) + .limit(limit ?? 0) + return results.map(resultToMigration) + } catch (err) { + return parseRepositoryError(err) + } + } + + const countByStatus = async ({ + cutoverVersion, + runId, + status, + }: { + cutoverVersion: number + runId: string + status: CashWalletMigrationStatus + }): Promise => { + try { + return CashWalletMigration.countDocuments({ cutoverVersion, runId, status }) + } catch (err) { + return parseRepositoryError(err) + } + } + + return { + getConfig, + updateConfig, + upsertMigration, + findMigrationByAccountId, + transitionMigration, + acquireMigrationLock, + releaseMigrationLock, + markMigrationFailed, + listRunnableMigrations, + countByStatus, + } +} diff --git a/src/services/mongoose/index.ts b/src/services/mongoose/index.ts index 8e5be4f68..e0cb83509 100644 --- a/src/services/mongoose/index.ts +++ b/src/services/mongoose/index.ts @@ -8,3 +8,4 @@ export * from "./wallet-invoices" export * from "./wallet-on-chain-addresses" export * from "./wallet-onchain-pending-receive" export * from "./merchants" +export * from "./cash-wallet-cutover" diff --git a/src/services/mongoose/schema.ts b/src/services/mongoose/schema.ts index ef7b0ebf9..a15023362 100644 --- a/src/services/mongoose/schema.ts +++ b/src/services/mongoose/schema.ts @@ -620,6 +620,68 @@ export const WalletOnChainPendingReceive = WalletOnChainPendingReceiveSchema, ) +const CashWalletCutoverConfigSchema = new Schema({ + _id: { type: String, default: "cash_wallet_cutover" }, + state: { type: String, enum: ["pre", "in_progress", "complete"], required: true }, + scheduledAt: Date, + startedAt: Date, + completedAt: Date, + pausedAt: Date, + pauseReason: String, + updatedBy: String, + cutoverVersion: { type: Number, required: true, default: 1 }, + runId: String, + updatedAt: { type: Date, default: Date.now }, +}) + +export const CashWalletCutoverConfig = mongoose.model( + "CashWalletCutoverConfig", + CashWalletCutoverConfigSchema, +) + +const CashWalletMigrationSchema = new Schema({ + _id: { type: String, required: true }, + accountId: { type: String, required: true, index: true }, + accountUuid: String, + legacyUsdWalletId: { type: String, required: true }, + destinationUsdtWalletId: { type: String, required: true }, + previousDefaultWalletId: String, + cutoverVersion: { type: Number, required: true, index: true }, + runId: { type: String, required: true, index: true }, + status: { type: String, required: true, index: true }, + sourceBalanceUsdCents: String, + destinationAmountUsdtMicros: String, + destinationStartingBalanceUsdtMicros: String, + feeAmountUsdCents: String, + feeAmountUsdtMicros: String, + balanceMoveInvoicePaymentRequest: String, + balanceMoveInvoicePaymentHash: String, + balanceMovePaymentTransactionId: String, + feeReimbursementInvoicePaymentRequest: String, + feeReimbursementInvoicePaymentHash: String, + feeReimbursementPaymentTransactionId: String, + estimatedFee: Boolean, + idempotencyKey: { type: String, required: true }, + attempts: { type: Number, default: 0 }, + lastError: String, + lockedAt: Date, + lockedBy: String, + startedAt: Date, + completedAt: Date, + updatedAt: { type: Date, default: Date.now, index: true }, +}) + +CashWalletMigrationSchema.index({ accountId: 1, runId: 1 }, { unique: true }) +CashWalletMigrationSchema.index({ idempotencyKey: 1 }, { unique: true }) +CashWalletMigrationSchema.index({ cutoverVersion: 1, status: 1, updatedAt: 1 }) +CashWalletMigrationSchema.index({ runId: 1, status: 1 }) +CashWalletMigrationSchema.index({ lockedAt: 1 }) + +export const CashWalletMigration = mongoose.model( + "CashWalletMigration", + CashWalletMigrationSchema, +) + const BridgeVirtualAccountSchema = new Schema({ // unique: true enforces one VA per account at the DB layer — idempotency guard accountId: { type: String, required: true, unique: true }, diff --git a/src/services/mongoose/schema.types.d.ts b/src/services/mongoose/schema.types.d.ts index 39d5a3da9..332c918f5 100644 --- a/src/services/mongoose/schema.types.d.ts +++ b/src/services/mongoose/schema.types.d.ts @@ -103,6 +103,52 @@ interface AccountRecord { save: () => Promise } +interface CashWalletCutoverConfigRecord { + _id: string + state: CashWalletCutoverState + scheduledAt?: Date + startedAt?: Date + completedAt?: Date + pausedAt?: Date + pauseReason?: string + updatedBy?: string + cutoverVersion: number + runId?: string + updatedAt: Date +} + +interface CashWalletMigrationRecord { + _id: string + accountId: string + accountUuid?: string + legacyUsdWalletId: string + destinationUsdtWalletId: string + previousDefaultWalletId?: string + cutoverVersion: number + runId: string + status: CashWalletMigrationStatus + sourceBalanceUsdCents?: string + destinationAmountUsdtMicros?: string + destinationStartingBalanceUsdtMicros?: string + feeAmountUsdCents?: string + feeAmountUsdtMicros?: string + balanceMoveInvoicePaymentRequest?: string + balanceMoveInvoicePaymentHash?: string + balanceMovePaymentTransactionId?: string + feeReimbursementInvoicePaymentRequest?: string + feeReimbursementInvoicePaymentHash?: string + feeReimbursementPaymentTransactionId?: string + estimatedFee?: boolean + idempotencyKey: string + attempts: number + lastError?: string + lockedAt?: Date + lockedBy?: string + startedAt?: Date + completedAt?: Date + updatedAt: Date +} + interface LocationRecord { type: "Point" coordinates: CoordinateRecord diff --git a/test/flash/unit/app/cash-wallet-cutover/amount-conversion.spec.ts b/test/flash/unit/app/cash-wallet-cutover/amount-conversion.spec.ts new file mode 100644 index 000000000..db42bc5f0 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/amount-conversion.spec.ts @@ -0,0 +1,50 @@ +import { + destinationShortfallUsdtMicros, + feeUsdCentsToUsdtMicros, + usdCentsToUsdtMicros, + usdtMicrosToUsdCentsCeil, +} from "@app/cash-wallet-cutover/amount-conversion" + +describe("cash wallet cutover amount conversion", () => { + it("converts USD cents to USDT micros exactly", () => { + expect(usdCentsToUsdtMicros("0")).toBe("0") + expect(usdCentsToUsdtMicros("1")).toBe("10000") + expect(usdCentsToUsdtMicros("100")).toBe("1000000") + expect(usdCentsToUsdtMicros("123456789")).toBe("1234567890000") + }) + + it("converts fee USD cents to USDT micros exactly", () => { + expect(feeUsdCentsToUsdtMicros("7")).toBe("70000") + }) + + it("rounds USDT micros up to USD cents for fee audit fields", () => { + expect(usdtMicrosToUsdCentsCeil("0")).toBe("0") + expect(usdtMicrosToUsdCentsCeil("1")).toBe("1") + expect(usdtMicrosToUsdCentsCeil("10000")).toBe("1") + expect(usdtMicrosToUsdCentsCeil("10001")).toBe("2") + }) + + it("computes destination USDT shortfall from the observed balance delta", () => { + expect( + destinationShortfallUsdtMicros({ + targetUsdtMicros: "10000000", + startingUsdtMicros: "5000000", + currentUsdtMicros: "14930000", + }), + ).toBe("70000") + expect( + destinationShortfallUsdtMicros({ + targetUsdtMicros: "10000000", + startingUsdtMicros: "5000000", + currentUsdtMicros: "15000000", + }), + ).toBe("0") + }) + + it("rejects invalid or fractional cent inputs", () => { + expect(usdCentsToUsdtMicros("1.5")).toBeInstanceOf(Error) + expect(usdCentsToUsdtMicros("abc")).toBeInstanceOf(Error) + expect(usdCentsToUsdtMicros("-1")).toBeInstanceOf(Error) + expect(usdtMicrosToUsdCentsCeil("1.5")).toBeInstanceOf(Error) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/client-capability.spec.ts b/test/flash/unit/app/cash-wallet-cutover/client-capability.spec.ts new file mode 100644 index 000000000..cfe6211e8 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/client-capability.spec.ts @@ -0,0 +1,46 @@ +import { + CASH_WALLET_USDT_CLIENT_CAPABILITY, + parseCashWalletClientCapabilities, +} from "@app/cash-wallet-cutover/client-capability" + +describe("cash wallet client capability parser", () => { + it("defaults missing headers to legacy compatibility", () => { + expect(parseCashWalletClientCapabilities({})).toEqual({ + cashWalletPresentation: "legacy_compat", + hasUsdtCashWalletSupport: false, + }) + }) + + it("treats unknown capabilities as legacy compatibility", () => { + expect( + parseCashWalletClientCapabilities({ + "x-flash-client-capabilities": "contacts-v2", + }), + ).toEqual({ + cashWalletPresentation: "legacy_compat", + hasUsdtCashWalletSupport: false, + }) + }) + + it("detects the USDT Cash Wallet capability", () => { + expect( + parseCashWalletClientCapabilities({ + "x-flash-client-capabilities": `contacts-v2, ${CASH_WALLET_USDT_CLIENT_CAPABILITY}`, + }), + ).toEqual({ + cashWalletPresentation: "usdt", + hasUsdtCashWalletSupport: true, + }) + }) + + it("accepts native client connection-param casing", () => { + expect( + parseCashWalletClientCapabilities({ + "X-Flash-Client-Capabilities": CASH_WALLET_USDT_CLIENT_CAPABILITY, + }), + ).toEqual({ + cashWalletPresentation: "usdt", + hasUsdtCashWalletSupport: true, + }) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/cutover-gate.spec.ts b/test/flash/unit/app/cash-wallet-cutover/cutover-gate.spec.ts new file mode 100644 index 000000000..356ce2a5f --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/cutover-gate.spec.ts @@ -0,0 +1,154 @@ +import { + CashWalletCutoverInProgressError, + CashWalletMigrationFailedError, + evaluateCashWalletCutoverGuard, + evaluateCashWalletCutoverPresentation, +} from "@app/cash-wallet-cutover/guard" + +const config = (state: CashWalletCutoverState): CashWalletCutoverConfig => ({ + state, + cutoverVersion: 2, + runId: "run-2", + updatedAt: new Date("2026-05-19T00:00:00Z"), +}) + +const migration = (status: CashWalletMigrationStatus): CashWalletMigration => ({ + id: "migration-id", + accountId: "account-id" as AccountId, + legacyUsdWalletId: "usd-wallet-id" as WalletId, + destinationUsdtWalletId: "usdt-wallet-id" as WalletId, + cutoverVersion: 2, + runId: "run-2", + status, + idempotencyKey: "run-2:account-id", + attempts: 0, + updatedAt: new Date("2026-05-19T00:00:00Z"), +}) + +const legacyClient = { + cashWalletPresentation: "legacy_compat" as const, + hasUsdtCashWalletSupport: false, +} + +const usdtClient = { + cashWalletPresentation: "usdt" as const, + hasUsdtCashWalletSupport: true, +} + +describe("cash wallet cutover guard", () => { + it("allows legacy route before cutover starts", () => { + expect(evaluateCashWalletCutoverGuard({ cutover: config("pre") })).toEqual({ + route: "legacy_usd", + }) + }) + + it("allows legacy route during cutover before this account starts", () => { + expect(evaluateCashWalletCutoverGuard({ cutover: config("in_progress") })).toEqual({ + route: "legacy_usd", + }) + expect( + evaluateCashWalletCutoverGuard({ + cutover: config("in_progress"), + migration: migration("not_started"), + }), + ).toEqual({ route: "legacy_usd" }) + }) + + it("rejects writes while this account is actively migrating", () => { + for (const status of [ + "balance_read", + "balance_move_sending", + "fee_reimbursement_sending", + ] as const) { + expect( + evaluateCashWalletCutoverGuard({ + cutover: config("in_progress"), + migration: migration(status), + }), + ).toBeInstanceOf(CashWalletCutoverInProgressError) + } + }) + + it("routes completed accounts to USDT during cutover", () => { + expect( + evaluateCashWalletCutoverGuard({ + cutover: config("in_progress"), + migration: migration("complete"), + }), + ).toEqual({ route: "usdt" }) + }) + + it("rejects failed and manual-review migrations", () => { + for (const status of ["failed", "requires_operator_review"] as const) { + expect( + evaluateCashWalletCutoverGuard({ + cutover: config("in_progress"), + migration: migration(status), + }), + ).toBeInstanceOf(CashWalletMigrationFailedError) + } + }) + + it("routes all accounts to USDT after global completion", () => { + expect(evaluateCashWalletCutoverGuard({ cutover: config("complete") })).toEqual({ + route: "usdt", + }) + }) +}) + +describe("cash wallet cutover presentation", () => { + it("presents legacy USD before cutover starts", () => { + expect( + evaluateCashWalletCutoverPresentation({ + cutover: config("pre"), + client: usdtClient, + }), + ).toEqual({ + presentation: "legacy_usd", + }) + }) + + it("presents completed accounts as legacy-compatible for old clients", () => { + expect( + evaluateCashWalletCutoverPresentation({ + cutover: config("in_progress"), + migration: migration("complete"), + client: legacyClient, + }), + ).toEqual({ + presentation: "legacy_usd_compat", + }) + }) + + it("presents completed accounts as USDT for capable clients", () => { + expect( + evaluateCashWalletCutoverPresentation({ + cutover: config("in_progress"), + migration: migration("complete"), + client: usdtClient, + }), + ).toEqual({ + presentation: "usdt", + }) + }) + + it("uses client capability after global completion", () => { + expect( + evaluateCashWalletCutoverPresentation({ + cutover: config("complete"), + client: legacyClient, + }), + ).toEqual({ + presentation: "legacy_usd_compat", + }) + + expect( + evaluateCashWalletCutoverPresentation({ + cutover: config("complete"), + client: usdtClient, + }), + ).toEqual({ + presentation: "usdt", + }) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/discovery-collector.spec.ts b/test/flash/unit/app/cash-wallet-cutover/discovery-collector.spec.ts new file mode 100644 index 000000000..201b382ba --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/discovery-collector.spec.ts @@ -0,0 +1,86 @@ +import { RepositoryError } from "@domain/errors" + +import { discoverCashWalletCutoverAccounts } from "@app/cash-wallet-cutover/discovery" + +import { WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" + +const account = (id: AccountId, defaultWalletId: WalletId): Account => + ({ + id, + uuid: `${id}-uuid` as AccountUuid, + defaultWalletId, + }) as Account + +const wallet = ({ + id, + accountId, + currency, +}: { + id: WalletId + accountId: AccountId + currency: WalletCurrency +}): Wallet => + ({ + id, + accountId, + type: WalletType.Checking, + currency, + onChainAddressIdentifiers: [], + onChainAddresses: () => [], + lnurlp: "lnurl" as Lnurl, + }) as Wallet + +async function* unlockedAccounts(accounts: Account[]): AsyncGenerator { + for (const account of accounts) yield account +} + +describe("cash wallet cutover account discovery collector", () => { + it("classifies every unlocked account with its wallets", async () => { + const accountOne = account("account-1" as AccountId, "account-1-usd" as WalletId) + const accountTwo = account("account-2" as AccountId, "account-2-usdt" as WalletId) + const walletsRepo = { + listByAccountId: jest.fn(async (accountId: AccountId) => [ + wallet({ + id: `${accountId}-usd` as WalletId, + accountId, + currency: WalletCurrency.Usd, + }), + wallet({ + id: `${accountId}-usdt` as WalletId, + accountId, + currency: WalletCurrency.Usdt, + }), + ]), + } + + const result = await discoverCashWalletCutoverAccounts({ + accountsRepo: { + listUnlockedAccounts: () => unlockedAccounts([accountOne, accountTwo]), + }, + walletsRepo, + }) + + expect(result).toEqual([ + expect.objectContaining({ accountId: "account-1", status: "legacy_default" }), + expect.objectContaining({ accountId: "account-2", status: "already_usdt" }), + ]) + expect(walletsRepo.listByAccountId).toHaveBeenCalledWith("account-1") + expect(walletsRepo.listByAccountId).toHaveBeenCalledWith("account-2") + }) + + it("returns repository errors without continuing discovery", async () => { + const accountOne = account("account-1" as AccountId, "account-1-usd" as WalletId) + const error = new RepositoryError("wallet lookup failed") + const walletsRepo = { + listByAccountId: jest.fn(async () => error), + } + + const result = await discoverCashWalletCutoverAccounts({ + accountsRepo: { listUnlockedAccounts: () => unlockedAccounts([accountOne]) }, + walletsRepo, + }) + + expect(result).toBe(error) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/discovery.spec.ts b/test/flash/unit/app/cash-wallet-cutover/discovery.spec.ts new file mode 100644 index 000000000..1606d9cad --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/discovery.spec.ts @@ -0,0 +1,111 @@ +import { classifyCashWalletsForCutover } from "@app/cash-wallet-cutover/discovery" + +import { WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" + +const account = (defaultWalletId: WalletId): Account => ({ + id: "account-id" as AccountId, + uuid: "account-uuid" as AccountUuid, + createdAt: new Date("2026-05-20T00:00:00Z"), + defaultWalletId, + username: "username" as Username, + npub: "npub" as Npub, + level: 1 as AccountLevel, + status: "active" as AccountStatus, + statusHistory: [{ status: "active" as AccountStatus, timestamp: new Date() }], + title: "" as BusinessMapTitle, + coordinates: undefined as Coordinates, + contactEnabled: false, + contacts: [], + withdrawFee: 0 as Satoshis, + isEditor: false, + notificationSettings: { push: { enabled: true, disabledCategories: [] } }, + quizQuestions: [], + quiz: [], + kratosUserId: "user-id" as UserId, + displayCurrency: "USD" as DisplayCurrency, +}) + +const wallet = ({ + id, + currency, + type = WalletType.Checking, +}: { + id: WalletId + currency: WalletCurrency + type?: WalletType +}): Wallet => ({ + id, + accountId: "account-id" as AccountId, + type, + currency, + onChainAddressIdentifiers: [], + onChainAddresses: () => [], + lnurlp: "lnurl" as Lnurl, +}) + +describe("cash wallet cutover discovery", () => { + const legacyUsdWallet = wallet({ + id: "legacy-usd-wallet-id" as WalletId, + currency: WalletCurrency.Usd, + }) + const destinationUsdtWallet = wallet({ + id: "usdt-wallet-id" as WalletId, + currency: WalletCurrency.Usdt, + }) + + it("classifies accounts whose default still points to legacy USD", () => { + const result = classifyCashWalletsForCutover({ + account: account("legacy-usd-wallet-id" as WalletId), + wallets: [legacyUsdWallet, destinationUsdtWallet], + }) + + expect(result).toMatchObject({ + status: "legacy_default", + accountId: "account-id", + accountUuid: "account-uuid", + legacyUsdWalletId: "legacy-usd-wallet-id", + destinationUsdtWalletId: "usdt-wallet-id", + previousDefaultWalletId: "legacy-usd-wallet-id", + }) + }) + + it("classifies accounts already defaulting to ETH-USDT", () => { + const result = classifyCashWalletsForCutover({ + account: account("usdt-wallet-id" as WalletId), + wallets: [legacyUsdWallet, destinationUsdtWallet], + }) + + expect(result).toMatchObject({ + status: "already_usdt", + legacyUsdWalletId: "legacy-usd-wallet-id", + destinationUsdtWalletId: "usdt-wallet-id", + previousDefaultWalletId: "usdt-wallet-id", + }) + }) + + it("classifies legacy USD wallets that are no longer the default as residual", () => { + const result = classifyCashWalletsForCutover({ + account: account("btc-wallet-id" as WalletId), + wallets: [legacyUsdWallet, destinationUsdtWallet], + }) + + expect(result).toMatchObject({ status: "residual_legacy_usd" }) + }) + + it("surfaces accounts that cannot be planned because a required cash wallet is missing", () => { + expect( + classifyCashWalletsForCutover({ + account: account("legacy-usd-wallet-id" as WalletId), + wallets: [legacyUsdWallet], + }), + ).toMatchObject({ status: "missing_destination_usdt" }) + + expect( + classifyCashWalletsForCutover({ + account: account("usdt-wallet-id" as WalletId), + wallets: [destinationUsdtWallet], + }), + ).toMatchObject({ status: "missing_legacy_usd" }) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/executor.spec.ts b/test/flash/unit/app/cash-wallet-cutover/executor.spec.ts new file mode 100644 index 000000000..60d5fae41 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/executor.spec.ts @@ -0,0 +1,82 @@ +import { CouldNotUpdateError } from "@domain/errors" + +import { executeCashWalletMigrationStep } from "@app/cash-wallet-cutover/executor" + +const migration = (status: CashWalletMigrationStatus): CashWalletMigration => ({ + id: "migration-id", + accountId: "account-id" as AccountId, + legacyUsdWalletId: "legacy-usd-wallet-id" as WalletId, + destinationUsdtWalletId: "usdt-wallet-id" as WalletId, + cutoverVersion: 7, + runId: "run-7", + status, + idempotencyKey: "cash-wallet-cutover:run-7:account-id", + attempts: 0, + updatedAt: new Date("2026-05-20T00:00:00Z"), +}) + +const handlers = () => ({ + not_started: jest.fn(async () => migration("started")), + started: jest.fn(async () => migration("provisioned")), + provisioned: jest.fn(async () => migration("balance_read")), + balance_read: jest.fn(async () => migration("invoice_created")), + invoice_created: jest.fn(async () => migration("balance_move_sending")), + balance_move_sending: jest.fn(async () => migration("balance_move_sent")), + balance_move_sent: jest.fn(async () => migration("balance_move_verified")), + balance_move_verified: jest.fn(async () => + migration("fee_reimbursement_invoice_created"), + ), + fee_reimbursement_invoice_created: jest.fn(async () => + migration("fee_reimbursement_sending"), + ), + fee_reimbursement_sending: jest.fn(async () => migration("fee_reimbursed")), + fee_reimbursed: jest.fn(async () => migration("pointer_flipped")), + pointer_flipped: jest.fn(async () => migration("legacy_zero_verified")), + legacy_zero_verified: jest.fn(async () => migration("complete")), +}) + +describe("cash wallet migration executor", () => { + it("dispatches a runnable migration to the handler for its current status", async () => { + const stepHandlers = handlers() + + const result = await executeCashWalletMigrationStep({ + migration: migration("invoice_created"), + handlers: stepHandlers, + }) + + expect(result).toMatchObject({ status: "balance_move_sending" }) + expect(stepHandlers.invoice_created).toHaveBeenCalledWith( + migration("invoice_created"), + ) + }) + + it("returns terminal migrations without invoking handlers", async () => { + const stepHandlers = handlers() + + const result = await executeCashWalletMigrationStep({ + migration: migration("requires_operator_review"), + handlers: stepHandlers, + }) + + expect(result).toMatchObject({ status: "requires_operator_review" }) + expect(Object.values(stepHandlers).some((handler) => handler.mock.calls.length)).toBe( + false, + ) + }) + + it("returns handler failures without trying a second checkpoint", async () => { + const error = new CouldNotUpdateError("checkpoint failed") + const stepHandlers = { + ...handlers(), + balance_move_sent: jest.fn(async () => error), + } + + const result = await executeCashWalletMigrationStep({ + migration: migration("balance_move_sent"), + handlers: stepHandlers, + }) + + expect(result).toBe(error) + expect(stepHandlers.balance_move_verified).not.toHaveBeenCalled() + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/handlers.spec.ts b/test/flash/unit/app/cash-wallet-cutover/handlers.spec.ts new file mode 100644 index 000000000..b807cd96f --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/handlers.spec.ts @@ -0,0 +1,231 @@ +import { createCashWalletMigrationStepHandlers } from "@app/cash-wallet-cutover/handlers" + +const migration = (status: CashWalletMigrationStatus): CashWalletMigration => ({ + id: "migration-id", + accountId: "account-id" as AccountId, + legacyUsdWalletId: "legacy-usd-wallet-id" as WalletId, + destinationUsdtWalletId: "usdt-wallet-id" as WalletId, + cutoverVersion: 7, + runId: "run-7", + status, + idempotencyKey: "cash-wallet-cutover:run-7:account-id", + attempts: 0, + updatedAt: new Date("2026-05-20T00:00:00Z"), +}) + +describe("cash wallet migration step handlers", () => { + it("builds handlers for every runnable status", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async ({ to }) => migration(to)), + } + const services = { + now: jest.fn(() => new Date("2026-05-20T16:00:00Z")), + provisioningService: { + ensureDestinationWallet: jest.fn(async () => true), + }, + balanceReader: { + readSourceBalanceUsdCents: jest.fn(async () => "1234"), + readDestinationBalanceUsdtMicros: jest.fn(async () => "5000000"), + }, + invoiceService: { + createInvoice: jest.fn( + async () => + ({ + paymentRequest: "lnbc1" as EncodedPaymentRequest, + paymentHash: "hash" as PaymentHash, + }) as LnInvoice, + ), + createNoAmountInvoice: jest.fn( + async () => + ({ + paymentRequest: "lnbc1-no-amount" as EncodedPaymentRequest, + paymentHash: "noAmountHash" as PaymentHash, + }) as LnInvoice, + ), + }, + paymentService: { + payInvoice: jest.fn(async () => ({ + transactionId: "ibex-tx-id" as IbexTransactionId, + })), + }, + balanceVerifier: { + verifyBalanceMove: jest.fn(async () => true), + }, + feeService: { + readFeeAmountUsdtMicros: jest.fn(async () => "70000"), + }, + treasuryService: { + getTreasuryWalletId: jest.fn(async () => "treasury-wallet-id" as WalletId), + }, + pointerService: { + flipDefaultWallet: jest.fn(async () => ({ + previousDefaultWalletId: "legacy-usd-wallet-id" as WalletId, + })), + }, + legacyWalletVerifier: { + verifyLegacyWalletZero: jest.fn(async () => true), + }, + } + + const handlers = createCashWalletMigrationStepHandlers({ + migrationsRepo, + services, + }) + + expect(Object.keys(handlers).sort()).toEqual([ + "balance_move_sending", + "balance_move_sent", + "balance_move_verified", + "balance_read", + "fee_reimbursed", + "fee_reimbursement_invoice_created", + "fee_reimbursement_sending", + "invoice_created", + "legacy_zero_verified", + "not_started", + "pointer_flipped", + "provisioned", + "started", + ]) + + await handlers.not_started(migration("not_started")) + await handlers.started(migration("started")) + await handlers.provisioned(migration("provisioned")) + await handlers.balance_move_verified(migration("balance_move_verified")) + + expect(services.now).toHaveBeenCalled() + expect(services.provisioningService.ensureDestinationWallet).toHaveBeenCalled() + expect(services.balanceReader.readSourceBalanceUsdCents).toHaveBeenCalledWith( + migration("provisioned"), + ) + expect(services.balanceReader.readDestinationBalanceUsdtMicros).toHaveBeenCalledWith( + migration("provisioned"), + ) + expect(services.feeService.readFeeAmountUsdtMicros).toHaveBeenCalledWith( + migration("balance_move_verified"), + ) + }) + + it("skips balance move and fee reimbursement for zero-balance migrations", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async ({ to }) => migration(to)), + } + const services = { + now: jest.fn(() => new Date("2026-05-20T16:00:00Z")), + provisioningService: { + ensureDestinationWallet: jest.fn(async () => true), + }, + balanceReader: { + readSourceBalanceUsdCents: jest.fn(async () => "0"), + readDestinationBalanceUsdtMicros: jest.fn(async () => "0"), + }, + invoiceService: { + createInvoice: jest.fn(), + createNoAmountInvoice: jest.fn(), + }, + paymentService: { + payInvoice: jest.fn(), + }, + balanceVerifier: { + verifyBalanceMove: jest.fn(), + }, + feeService: { + readFeeAmountUsdtMicros: jest.fn(), + }, + treasuryService: { + getTreasuryWalletId: jest.fn(), + }, + pointerService: { + flipDefaultWallet: jest.fn(async () => ({ + previousDefaultWalletId: "legacy-usd-wallet-id" as WalletId, + })), + }, + legacyWalletVerifier: { + verifyLegacyWalletZero: jest.fn(async () => true), + }, + } + + const handlers = createCashWalletMigrationStepHandlers({ + migrationsRepo, + services, + }) + + const result = await handlers.balance_read({ + ...migration("balance_read"), + sourceBalanceUsdCents: "0", + destinationAmountUsdtMicros: "0", + }) + + expect(result).toMatchObject({ status: "pointer_flipped" }) + expect(services.pointerService.flipDefaultWallet).toHaveBeenCalledWith({ + accountId: "account-id", + destinationWalletId: "usdt-wallet-id", + }) + expect(services.invoiceService.createInvoice).not.toHaveBeenCalled() + expect(services.invoiceService.createNoAmountInvoice).not.toHaveBeenCalled() + expect(services.paymentService.payInvoice).not.toHaveBeenCalled() + expect(services.balanceVerifier.verifyBalanceMove).not.toHaveBeenCalled() + expect(services.feeService.readFeeAmountUsdtMicros).not.toHaveBeenCalled() + expect(services.treasuryService.getTreasuryWalletId).not.toHaveBeenCalled() + }) + + it("skips fee reimbursement invoice creation when the destination shortfall is zero", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async ({ to, patch }) => ({ + ...migration(to), + ...patch, + })), + } + const services = { + now: jest.fn(() => new Date("2026-05-20T16:00:00Z")), + provisioningService: { + ensureDestinationWallet: jest.fn(async () => true), + }, + balanceReader: { + readSourceBalanceUsdCents: jest.fn(async () => "1000"), + readDestinationBalanceUsdtMicros: jest.fn(async () => "0"), + }, + invoiceService: { + createInvoice: jest.fn(), + createNoAmountInvoice: jest.fn(), + }, + paymentService: { + payInvoice: jest.fn(), + }, + balanceVerifier: { + verifyBalanceMove: jest.fn(async () => true), + }, + feeService: { + readFeeAmountUsdtMicros: jest.fn(async () => "0"), + }, + treasuryService: { + getTreasuryWalletId: jest.fn(), + }, + pointerService: { + flipDefaultWallet: jest.fn(async () => ({ + previousDefaultWalletId: "legacy-usd-wallet-id" as WalletId, + })), + }, + legacyWalletVerifier: { + verifyLegacyWalletZero: jest.fn(async () => true), + }, + } + + const handlers = createCashWalletMigrationStepHandlers({ + migrationsRepo, + services, + }) + + const result = await handlers.balance_move_verified( + migration("balance_move_verified"), + ) + + expect(result).toMatchObject({ + status: "fee_reimbursed", + feeAmountUsdCents: "0", + feeAmountUsdtMicros: "0", + }) + expect(services.invoiceService.createInvoice).not.toHaveBeenCalled() + expect(services.treasuryService.getTreasuryWalletId).not.toHaveBeenCalled() + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/lifecycle.spec.ts b/test/flash/unit/app/cash-wallet-cutover/lifecycle.spec.ts new file mode 100644 index 000000000..ff578a24d --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/lifecycle.spec.ts @@ -0,0 +1,190 @@ +jest.mock("@services/mongoose", () => ({ + CashWalletCutoverRepository: jest.fn(), +})) + +import { + completePrimaryCashWalletCutover, + getPrimaryCashWalletCutoverStatus, + startPrimaryCashWalletCutover, +} from "@app/cash-wallet-cutover/lifecycle" +import { + CashWalletCutoverInProgressError, + CashWalletMigrationFailedError, + InvalidCashWalletCutoverStateTransitionError, +} from "@app/cash-wallet-cutover/errors" + +const config = (state: CashWalletCutoverState): CashWalletCutoverConfig => ({ + state, + cutoverVersion: 7, + runId: "run-7", + updatedAt: new Date("2026-05-20T00:00:00Z"), +}) + +const repo = ({ + currentConfig = config("pre"), + runnable = [], + counts = {}, +}: { + currentConfig?: CashWalletCutoverConfig + runnable?: CashWalletMigration[] + counts?: Partial> +} = {}) => ({ + getConfig: jest.fn(async () => currentConfig), + updateConfig: jest.fn(async (patch: Partial) => ({ + ...currentConfig, + ...patch, + })), + listRunnableMigrations: jest.fn(async () => runnable), + countByStatus: jest.fn( + async ({ status }: { status: CashWalletMigrationStatus }) => counts[status] ?? 0, + ), +}) + +describe("cash wallet cutover lifecycle", () => { + const now = new Date("2026-05-20T12:00:00Z") + + it("starts a prepared cutover run", async () => { + const migrationsRepo = repo() + + const result = await startPrimaryCashWalletCutover({ + cutoverVersion: 7, + runId: "run-7", + actor: "operator", + now, + migrationsRepo, + }) + + expect(migrationsRepo.updateConfig).toHaveBeenCalledWith( + expect.objectContaining({ + state: "in_progress", + cutoverVersion: 7, + runId: "run-7", + startedAt: now, + }), + "operator", + ) + expect(result).toMatchObject({ state: "in_progress", runId: "run-7" }) + }) + + it("is idempotent for the active cutover run", async () => { + const migrationsRepo = repo({ currentConfig: config("in_progress") }) + + const result = await startPrimaryCashWalletCutover({ + cutoverVersion: 7, + runId: "run-7", + actor: "operator", + now, + migrationsRepo, + }) + + expect(migrationsRepo.updateConfig).not.toHaveBeenCalled() + expect(result).toEqual(config("in_progress")) + }) + + it("rejects starting a different run while one is active", async () => { + const migrationsRepo = repo({ currentConfig: config("in_progress") }) + + const result = await startPrimaryCashWalletCutover({ + cutoverVersion: 8, + runId: "run-8", + actor: "operator", + now, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(CashWalletCutoverInProgressError) + }) + + it("rejects restarting a completed cutover", async () => { + const migrationsRepo = repo({ currentConfig: config("complete") }) + + const result = await startPrimaryCashWalletCutover({ + cutoverVersion: 7, + runId: "run-7", + actor: "operator", + now, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(InvalidCashWalletCutoverStateTransitionError) + }) + + it("refuses completion while runnable migrations remain", async () => { + const migrationsRepo = repo({ + currentConfig: config("in_progress"), + runnable: [{ id: "migration-id", status: "started" } as CashWalletMigration], + }) + + const result = await completePrimaryCashWalletCutover({ + cutoverVersion: 7, + runId: "run-7", + actor: "operator", + now, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(CashWalletCutoverInProgressError) + expect(migrationsRepo.updateConfig).not.toHaveBeenCalled() + }) + + it("refuses completion when failed migrations exist", async () => { + const migrationsRepo = repo({ + currentConfig: config("in_progress"), + counts: { failed: 1 }, + }) + + const result = await completePrimaryCashWalletCutover({ + cutoverVersion: 7, + runId: "run-7", + actor: "operator", + now, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(CashWalletMigrationFailedError) + }) + + it("marks cutover complete after all migrations are terminal-success", async () => { + const migrationsRepo = repo({ currentConfig: config("in_progress") }) + + const result = await completePrimaryCashWalletCutover({ + cutoverVersion: 7, + runId: "run-7", + actor: "operator", + now, + migrationsRepo, + }) + + expect(migrationsRepo.updateConfig).toHaveBeenCalledWith( + expect.objectContaining({ + state: "complete", + cutoverVersion: 7, + runId: "run-7", + completedAt: now, + }), + "operator", + ) + expect(result).toMatchObject({ state: "complete" }) + }) + + it("returns non-zero migration counts for status checks", async () => { + const migrationsRepo = repo({ + currentConfig: config("in_progress"), + counts: { complete: 10, failed: 1 }, + }) + + const result = await getPrimaryCashWalletCutoverStatus({ + cutoverVersion: 7, + runId: "run-7", + migrationsRepo, + }) + + expect(result).toEqual({ + config: config("in_progress"), + countsByStatus: { + complete: 10, + failed: 1, + }, + }) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/migration-records.spec.ts b/test/flash/unit/app/cash-wallet-cutover/migration-records.spec.ts new file mode 100644 index 000000000..331f0dbb4 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/migration-records.spec.ts @@ -0,0 +1,70 @@ +import { RepositoryError } from "@domain/errors" + +import { upsertPrimaryCashWalletMigrationRecords } from "@app/cash-wallet-cutover/migration-records" + +const plan = (accountId: AccountId): PrimaryCashWalletMigrationPlan => ({ + accountId, + accountUuid: `${accountId}-uuid` as AccountUuid, + legacyUsdWalletId: `${accountId}-usd` as WalletId, + destinationUsdtWalletId: `${accountId}-usdt` as WalletId, + previousDefaultWalletId: `${accountId}-default` as WalletId, + cutoverVersion: 5, + runId: "run-5", + idempotencyKey: `cash-wallet-cutover:run-5:${accountId}`, +}) + +describe("cash wallet migration record upsert", () => { + it("upserts one not-started migration record for each primary plan", async () => { + const migrationsRepo = { + upsertMigration: jest.fn(async (args) => ({ + id: `${args.accountId}-migration`, + ...args, + status: "not_started" as CashWalletMigrationStatus, + attempts: 0, + updatedAt: new Date("2026-05-20T00:00:00Z"), + })), + } + + const result = await upsertPrimaryCashWalletMigrationRecords({ + migrationsRepo, + plans: [plan("account-1" as AccountId), plan("account-2" as AccountId)], + }) + + expect(result).toEqual([ + expect.objectContaining({ id: "account-1-migration", accountId: "account-1" }), + expect.objectContaining({ id: "account-2-migration", accountId: "account-2" }), + ]) + expect(migrationsRepo.upsertMigration).toHaveBeenCalledTimes(2) + expect(migrationsRepo.upsertMigration).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + accountId: "account-1", + accountUuid: "account-1-uuid", + legacyUsdWalletId: "account-1-usd", + destinationUsdtWalletId: "account-1-usdt", + previousDefaultWalletId: "account-1-default", + cutoverVersion: 5, + runId: "run-5", + idempotencyKey: "cash-wallet-cutover:run-5:account-1", + }), + ) + }) + + it("returns repository errors and stops creating more records", async () => { + const error = new RepositoryError("could not upsert migration") + const migrationsRepo = { + upsertMigration: jest + .fn() + .mockResolvedValueOnce(error) + .mockResolvedValueOnce({} as CashWalletMigration), + } + + const result = await upsertPrimaryCashWalletMigrationRecords({ + migrationsRepo, + plans: [plan("account-1" as AccountId), plan("account-2" as AccountId)], + }) + + expect(result).toBe(error) + expect(migrationsRepo.upsertMigration).toHaveBeenCalledTimes(1) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/migration-state-machine.spec.ts b/test/flash/unit/app/cash-wallet-cutover/migration-state-machine.spec.ts new file mode 100644 index 000000000..316fa2443 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/migration-state-machine.spec.ts @@ -0,0 +1,55 @@ +import { + assertCanTransition, + nextResumeStatus, +} from "@app/cash-wallet-cutover/state-machine" + +describe("cash wallet cutover migration state machine", () => { + it("allows the happy-path checkpoint order", () => { + const statuses = [ + "not_started", + "started", + "provisioned", + "balance_read", + "invoice_created", + "balance_move_sending", + "balance_move_sent", + "balance_move_verified", + "fee_reimbursement_invoice_created", + "fee_reimbursement_sending", + "fee_reimbursed", + "pointer_flipped", + "legacy_zero_verified", + "complete", + ] as const + + for (let i = 0; i < statuses.length - 1; i++) { + expect(assertCanTransition(statuses[i], statuses[i + 1])).toBe(true) + } + }) + + it("rejects pointer flip before fee reimbursement", () => { + expect( + assertCanTransition("balance_move_verified", "pointer_flipped"), + ).toBeInstanceOf(Error) + }) + + it("allows skipping fee reimbursement when there is no shortfall", () => { + expect(assertCanTransition("balance_move_verified", "fee_reimbursed")).toBe(true) + }) + + it("resumes from stored checkpoint without repeating completed side effects", () => { + expect(nextResumeStatus("invoice_created")).toBe("invoice_created") + expect(nextResumeStatus("balance_move_sent")).toBe("balance_move_sent") + expect(nextResumeStatus("fee_reimbursement_invoice_created")).toBe( + "fee_reimbursement_invoice_created", + ) + }) + + it("does not progress terminal/manual-review states without override", () => { + expect(assertCanTransition("complete", "started")).toBeInstanceOf(Error) + expect(assertCanTransition("failed", "started")).toBeInstanceOf(Error) + expect(assertCanTransition("requires_operator_review", "started")).toBeInstanceOf( + Error, + ) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/orchestrator.spec.ts b/test/flash/unit/app/cash-wallet-cutover/orchestrator.spec.ts new file mode 100644 index 000000000..32811fb58 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/orchestrator.spec.ts @@ -0,0 +1,84 @@ +jest.mock("@app/accounts", () => ({ + addWalletIfNonexistent: jest.fn(), + updateDefaultWalletId: jest.fn(), +})) +jest.mock("@app/wallets", () => ({ + addInvoiceForRecipientForUsdWallet: jest.fn(), + addInvoiceNoAmountForRecipient: jest.fn(), + getBalanceForWallet: jest.fn(), +})) +jest.mock("@services/mongoose", () => ({ + AccountsRepository: jest.fn(() => ({ findById: jest.fn() })), + CashWalletCutoverRepository: jest.fn(), +})) +jest.mock("@services/ibex/client", () => ({ + __esModule: true, + default: { + payInvoice: jest.fn(), + getTransactionDetails: jest.fn(), + }, +})) + +import { runPrimaryCashWalletCutoverBatch } from "@app/cash-wallet-cutover/orchestrator" + +const migration = (status: CashWalletMigrationStatus): CashWalletMigration => ({ + id: "migration-id", + accountId: "account-id" as AccountId, + legacyUsdWalletId: "legacy-usd-wallet-id" as WalletId, + destinationUsdtWalletId: "usdt-wallet-id" as WalletId, + cutoverVersion: 7, + runId: "run-7", + status, + idempotencyKey: "cash-wallet-cutover:run-7:account-id", + attempts: 0, + updatedAt: new Date("2026-05-20T00:00:00Z"), +}) + +describe("primary cash wallet cutover orchestrator", () => { + it("runs a locked batch with default step handlers", async () => { + const started = migration("not_started") + const locked = migration("not_started") + const migrationsRepo = { + transitionMigration: jest.fn(async () => migration("started")), + listRunnableMigrations: jest.fn(async () => [started]), + acquireMigrationLock: jest.fn(async () => locked), + markMigrationFailed: jest.fn(), + releaseMigrationLock: jest.fn(async () => locked), + } + const runtimeServices = { + now: jest.fn(() => new Date("2026-05-20T16:00:00Z")), + provisioningService: { ensureDestinationWallet: jest.fn() }, + balanceReader: { + readSourceBalanceUsdCents: jest.fn(), + readDestinationBalanceUsdtMicros: jest.fn(), + }, + invoiceService: { createInvoice: jest.fn(), createNoAmountInvoice: jest.fn() }, + paymentService: { payInvoice: jest.fn() }, + balanceVerifier: { verifyBalanceMove: jest.fn() }, + feeService: { readFeeAmountUsdtMicros: jest.fn() }, + treasuryService: { getTreasuryWalletId: jest.fn() }, + pointerService: { flipDefaultWallet: jest.fn() }, + legacyWalletVerifier: { verifyLegacyWalletZero: jest.fn() }, + } + + const result = await runPrimaryCashWalletCutoverBatch({ + cutoverVersion: 7, + runId: "run-7", + workerId: "worker-1", + limit: 5, + lockStaleBefore: new Date("2026-05-20T15:00:00Z"), + migrationsRepo, + runtimeServices, + }) + + expect(result).toEqual({ attempted: 1, advanced: 1, failed: 0, skipped: 0 }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "not_started", + to: "started", + cutoverVersion: 7, + runId: "run-7", + patch: { startedAt: new Date("2026-05-20T16:00:00Z") }, + }) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/planner.spec.ts b/test/flash/unit/app/cash-wallet-cutover/planner.spec.ts new file mode 100644 index 000000000..61f492a1b --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/planner.spec.ts @@ -0,0 +1,51 @@ +import { buildPrimaryCashWalletMigrationPlan } from "@app/cash-wallet-cutover/planner" + +const discovery = ( + status: CashWalletCutoverDiscoveryStatus, + accountId: AccountId, +): CashWalletCutoverDiscovery => ({ + status, + accountId, + accountUuid: `${accountId}-uuid` as AccountUuid, + legacyUsdWalletId: `${accountId}-usd` as WalletId, + destinationUsdtWalletId: `${accountId}-usdt` as WalletId, + previousDefaultWalletId: `${accountId}-default` as WalletId, +}) + +describe("primary cash wallet migration planner", () => { + it("creates deterministic migration plans for legacy-default accounts only", () => { + const result = buildPrimaryCashWalletMigrationPlan({ + cutoverVersion: 4, + runId: "run-4", + discoveries: [ + discovery("legacy_default", "account-1" as AccountId), + discovery("already_usdt", "account-2" as AccountId), + discovery("residual_legacy_usd", "account-3" as AccountId), + discovery("legacy_default", "account-4" as AccountId), + ], + }) + + expect(result).toEqual([ + { + accountId: "account-1", + accountUuid: "account-1-uuid", + legacyUsdWalletId: "account-1-usd", + destinationUsdtWalletId: "account-1-usdt", + previousDefaultWalletId: "account-1-default", + cutoverVersion: 4, + runId: "run-4", + idempotencyKey: "cash-wallet-cutover:run-4:account-1", + }, + { + accountId: "account-4", + accountUuid: "account-4-uuid", + legacyUsdWalletId: "account-4-usd", + destinationUsdtWalletId: "account-4-usdt", + previousDefaultWalletId: "account-4-default", + cutoverVersion: 4, + runId: "run-4", + idempotencyKey: "cash-wallet-cutover:run-4:account-4", + }, + ]) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/preflight.spec.ts b/test/flash/unit/app/cash-wallet-cutover/preflight.spec.ts new file mode 100644 index 000000000..66a1a5aad --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/preflight.spec.ts @@ -0,0 +1,65 @@ +import { buildCashWalletCutoverPreflightReport } from "@app/cash-wallet-cutover/preflight" + +const discovery = ( + status: CashWalletCutoverDiscoveryStatus, + accountId = `${status}-account` as AccountId, +): CashWalletCutoverDiscovery => ({ + status, + accountId, + accountUuid: `${accountId}-uuid` as AccountUuid, + legacyUsdWalletId: + status === "missing_legacy_usd" ? undefined : (`${accountId}-usd` as WalletId), + destinationUsdtWalletId: + status === "missing_destination_usdt" ? undefined : (`${accountId}-usdt` as WalletId), + previousDefaultWalletId: `${accountId}-default` as WalletId, +}) + +describe("cash wallet cutover preflight report", () => { + it("counts migration candidates and non-migrating classifications", () => { + const report = buildCashWalletCutoverPreflightReport({ + cutoverVersion: 3, + runId: "run-3", + discoveries: [ + discovery("legacy_default", "legacy-1" as AccountId), + discovery("legacy_default", "legacy-2" as AccountId), + discovery("already_usdt"), + discovery("residual_legacy_usd"), + discovery("missing_legacy_usd"), + discovery("missing_destination_usdt"), + ], + }) + + expect(report).toMatchObject({ + cutoverVersion: 3, + runId: "run-3", + totalAccounts: 6, + migrationCandidates: 2, + alreadyUsdt: 1, + residualLegacyUsd: 1, + blockers: 2, + canStart: false, + }) + expect(report.blockerAccounts).toEqual([ + { accountId: "missing_legacy_usd-account", reason: "missing_legacy_usd" }, + { + accountId: "missing_destination_usdt-account", + reason: "missing_destination_usdt", + }, + ]) + }) + + it("allows start when every account is either migratable, already migrated, or residual", () => { + const report = buildCashWalletCutoverPreflightReport({ + cutoverVersion: 3, + runId: "run-3", + discoveries: [ + discovery("legacy_default"), + discovery("already_usdt"), + discovery("residual_legacy_usd"), + ], + }) + + expect(report.canStart).toBe(true) + expect(report.blockerAccounts).toEqual([]) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/prepare.spec.ts b/test/flash/unit/app/cash-wallet-cutover/prepare.spec.ts new file mode 100644 index 000000000..137c373dc --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/prepare.spec.ts @@ -0,0 +1,123 @@ +import { RepositoryError } from "@domain/errors" + +import { preparePrimaryCashWalletCutover } from "@app/cash-wallet-cutover/prepare" + +import { WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" + +const account = (id: AccountId, defaultWalletId: WalletId): Account => + ({ + id, + uuid: `${id}-uuid` as AccountUuid, + defaultWalletId, + }) as Account + +const wallet = (accountId: AccountId, id: WalletId, currency: WalletCurrency): Wallet => + ({ + id, + accountId, + type: WalletType.Checking, + currency, + onChainAddressIdentifiers: [], + onChainAddresses: () => [], + lnurlp: "lnurl" as Lnurl, + }) as Wallet + +async function* unlockedAccounts(accounts: Account[]): AsyncGenerator { + for (const account of accounts) yield account +} + +describe("prepare primary cash wallet cutover", () => { + it("discovers accounts, builds preflight, and upserts primary migration records", async () => { + const accountOne = account("account-1" as AccountId, "account-1-usd" as WalletId) + const accountTwo = account("account-2" as AccountId, "account-2-usdt" as WalletId) + const walletsRepo = { + listByAccountId: jest.fn(async (accountId: AccountId) => [ + wallet(accountId, `${accountId}-usd` as WalletId, WalletCurrency.Usd), + wallet(accountId, `${accountId}-usdt` as WalletId, WalletCurrency.Usdt), + ]), + } + const migrationsRepo = { + upsertMigration: jest.fn(async (plan: PrimaryCashWalletMigrationPlan) => ({ + id: `${plan.accountId}-migration`, + ...plan, + status: "not_started" as CashWalletMigrationStatus, + attempts: 0, + updatedAt: new Date("2026-05-20T00:00:00Z"), + })), + } + + const result = await preparePrimaryCashWalletCutover({ + cutoverVersion: 6, + runId: "run-6", + accountsRepo: { + listUnlockedAccounts: () => unlockedAccounts([accountOne, accountTwo]), + }, + walletsRepo, + migrationsRepo, + }) + + expect(result).toMatchObject({ + report: { + totalAccounts: 2, + migrationCandidates: 1, + alreadyUsdt: 1, + blockers: 0, + canStart: true, + }, + plannedMigrations: [ + expect.objectContaining({ + accountId: "account-1", + idempotencyKey: "cash-wallet-cutover:run-6:account-1", + }), + ], + migrations: [expect.objectContaining({ id: "account-1-migration" })], + }) + expect(migrationsRepo.upsertMigration).toHaveBeenCalledTimes(1) + }) + + it("does not create migration records when preflight has blockers", async () => { + const blockedAccount = account("account-1" as AccountId, "account-1-usd" as WalletId) + const migrationsRepo = { + upsertMigration: jest.fn(), + } + + const result = await preparePrimaryCashWalletCutover({ + cutoverVersion: 6, + runId: "run-6", + accountsRepo: { listUnlockedAccounts: () => unlockedAccounts([blockedAccount]) }, + walletsRepo: { + listByAccountId: jest.fn(async (accountId: AccountId) => [ + wallet(accountId, `${accountId}-usd` as WalletId, WalletCurrency.Usd), + ]), + }, + migrationsRepo, + }) + + expect(result).toMatchObject({ + report: { + blockers: 1, + canStart: false, + }, + plannedMigrations: [], + migrations: [], + }) + expect(migrationsRepo.upsertMigration).not.toHaveBeenCalled() + }) + + it("returns repository errors from discovery", async () => { + const error = new RepositoryError("wallet lookup failed") + const result = await preparePrimaryCashWalletCutover({ + cutoverVersion: 6, + runId: "run-6", + accountsRepo: { + listUnlockedAccounts: () => + unlockedAccounts([account("account-1" as AccountId, "wallet-id" as WalletId)]), + }, + walletsRepo: { listByAccountId: jest.fn(async () => error) }, + migrationsRepo: { upsertMigration: jest.fn() }, + }) + + expect(result).toBe(error) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/presentation-for-account.spec.ts b/test/flash/unit/app/cash-wallet-cutover/presentation-for-account.spec.ts new file mode 100644 index 000000000..7e9557733 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/presentation-for-account.spec.ts @@ -0,0 +1,135 @@ +jest.mock("@services/mongoose", () => ({ + CashWalletCutoverRepository: jest.fn(), + WalletsRepository: jest.fn(), +})) + +import { + resolveCashWalletMutationWalletIdForAccount, + resolveCashWalletPresentationForAccount, +} from "@app/cash-wallet-cutover/presentation-for-account" +import { WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" + +const account = { id: "account-id", defaultWalletId: "legacy-usd-wallet-id" } as Account + +const wallet = ({ id, currency }: { id: string; currency: WalletCurrency }): Wallet => + ({ + id, + accountId: account.id, + type: WalletType.Checking, + currency, + }) as Wallet + +const legacyUsdWallet = wallet({ + id: "legacy-usd-wallet-id", + currency: WalletCurrency.Usd, +}) +const usdtWallet = wallet({ + id: "usdt-wallet-id", + currency: WalletCurrency.Usdt, +}) + +const config = (state: CashWalletCutoverState): CashWalletCutoverConfig => ({ + state, + cutoverVersion: 2, + runId: "run-2", + updatedAt: new Date("2026-05-19T00:00:00Z"), +}) + +const migration = (status: CashWalletMigrationStatus): CashWalletMigration => ({ + id: "migration-id", + accountId: account.id, + legacyUsdWalletId: legacyUsdWallet.id, + destinationUsdtWalletId: usdtWallet.id, + cutoverVersion: 2, + runId: "run-2", + status, + idempotencyKey: "run-2:account-id", + attempts: 0, + updatedAt: new Date("2026-05-19T00:00:00Z"), +}) + +describe("cash wallet presentation for account", () => { + it("uses existing migration lookup and presents old clients as legacy-compatible after migration", async () => { + const migrationsRepo = { + getConfig: jest.fn(async () => config("in_progress")), + findMigrationByAccountId: jest.fn(async () => migration("complete")), + } + const walletsRepo = { + listByAccountId: jest.fn(async () => [legacyUsdWallet, usdtWallet]), + } + + const result = await resolveCashWalletPresentationForAccount({ + account, + client: { + cashWalletPresentation: "legacy_compat", + hasUsdtCashWalletSupport: false, + }, + migrationsRepo, + walletsRepo, + }) + + expect(result).toEqual({ + wallets: [legacyUsdWallet], + defaultWalletId: legacyUsdWallet.id, + legacyUsdWallet, + activeSettlementWallet: usdtWallet, + }) + expect(migrationsRepo.findMigrationByAccountId).toHaveBeenCalledWith({ + accountId: account.id, + cutoverVersion: 2, + runId: "run-2", + }) + }) + + it("does not require migration lookup after global completion", async () => { + const migrationsRepo = { + getConfig: jest.fn(async () => config("complete")), + findMigrationByAccountId: jest.fn(), + } + const walletsRepo = { + listByAccountId: jest.fn(async () => [legacyUsdWallet, usdtWallet]), + } + + const result = await resolveCashWalletPresentationForAccount({ + account, + client: { + cashWalletPresentation: "usdt", + hasUsdtCashWalletSupport: true, + }, + migrationsRepo, + walletsRepo, + }) + + expect(result).toEqual({ + wallets: [usdtWallet], + defaultWalletId: usdtWallet.id, + legacyUsdWallet, + activeSettlementWallet: usdtWallet, + }) + expect(migrationsRepo.findMigrationByAccountId).not.toHaveBeenCalled() + }) + + it("routes old-client legacy USD mutation wallet ids to the active settlement wallet", async () => { + const migrationsRepo = { + getConfig: jest.fn(async () => config("in_progress")), + findMigrationByAccountId: jest.fn(async () => migration("complete")), + } + const walletsRepo = { + listByAccountId: jest.fn(async () => [legacyUsdWallet, usdtWallet]), + } + + const result = await resolveCashWalletMutationWalletIdForAccount({ + account, + walletId: legacyUsdWallet.id, + client: { + cashWalletPresentation: "legacy_compat", + hasUsdtCashWalletSupport: false, + }, + migrationsRepo, + walletsRepo, + }) + + expect(result).toBe(usdtWallet.id) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/presentation.spec.ts b/test/flash/unit/app/cash-wallet-cutover/presentation.spec.ts new file mode 100644 index 000000000..8eb42fdc1 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/presentation.spec.ts @@ -0,0 +1,120 @@ +import { + CashWalletMissingLegacyUsdWalletError, + CashWalletMissingUsdtWalletError, +} from "@app/cash-wallet-cutover/errors" +import { + cashWalletTransactionWalletIdsForPresentation, + resolveCashWalletPresentation, +} from "@app/cash-wallet-cutover/presentation" +import { WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" + +const wallet = ({ id, currency }: { id: string; currency: WalletCurrency }): Wallet => + ({ + id, + accountId: "account-id", + type: WalletType.Checking, + currency, + }) as Wallet + +const legacyUsdWallet = wallet({ + id: "legacy-usd-wallet-id", + currency: WalletCurrency.Usd, +}) +const usdtWallet = wallet({ + id: "usdt-wallet-id", + currency: WalletCurrency.Usdt, +}) +const btcWallet = wallet({ + id: "btc-wallet-id", + currency: WalletCurrency.Btc, +}) + +describe("cash wallet presentation resolver", () => { + it("returns the legacy USD wallet as the active settlement wallet before cutover", () => { + expect( + resolveCashWalletPresentation({ + decision: { presentation: "legacy_usd" }, + wallets: [btcWallet, legacyUsdWallet, usdtWallet], + }), + ).toEqual({ + wallets: [btcWallet, legacyUsdWallet], + defaultWalletId: legacyUsdWallet.id, + legacyUsdWallet, + activeSettlementWallet: legacyUsdWallet, + }) + }) + + it("presents legacy USD while routing settlement to USDT for old clients after migration", () => { + expect( + resolveCashWalletPresentation({ + decision: { presentation: "legacy_usd_compat" }, + wallets: [btcWallet, legacyUsdWallet, usdtWallet], + }), + ).toEqual({ + wallets: [btcWallet, legacyUsdWallet], + defaultWalletId: legacyUsdWallet.id, + legacyUsdWallet, + activeSettlementWallet: usdtWallet, + }) + }) + + it("presents the USDT wallet directly for capable clients", () => { + expect( + resolveCashWalletPresentation({ + decision: { presentation: "usdt" }, + wallets: [btcWallet, legacyUsdWallet, usdtWallet], + }), + ).toEqual({ + wallets: [btcWallet, usdtWallet], + defaultWalletId: usdtWallet.id, + legacyUsdWallet, + activeSettlementWallet: usdtWallet, + }) + }) + + it("returns cutover-state errors for missing presentation wallets", () => { + expect( + resolveCashWalletPresentation({ + decision: { presentation: "legacy_usd" }, + wallets: [usdtWallet], + }), + ).toBeInstanceOf(CashWalletMissingLegacyUsdWalletError) + + expect( + resolveCashWalletPresentation({ + decision: { presentation: "legacy_usd_compat" }, + wallets: [legacyUsdWallet], + }), + ).toBeInstanceOf(CashWalletMissingUsdtWalletError) + }) +}) + +describe("cash wallet transaction wallet ids for presentation", () => { + it("uses the active settlement wallet when defaulting legacy-compatible history", () => { + expect( + cashWalletTransactionWalletIdsForPresentation({ + presentation: { + wallets: [btcWallet, legacyUsdWallet], + defaultWalletId: legacyUsdWallet.id, + legacyUsdWallet, + activeSettlementWallet: usdtWallet, + }, + }), + ).toEqual([btcWallet.id, usdtWallet.id]) + }) + + it("remaps explicit legacy USD wallet ids to active settlement wallet ids", () => { + expect( + cashWalletTransactionWalletIdsForPresentation({ + walletIds: [legacyUsdWallet.id], + presentation: { + wallets: [btcWallet, legacyUsdWallet], + defaultWalletId: legacyUsdWallet.id, + legacyUsdWallet, + activeSettlementWallet: usdtWallet, + }, + }), + ).toEqual([usdtWallet.id]) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/preview.spec.ts b/test/flash/unit/app/cash-wallet-cutover/preview.spec.ts new file mode 100644 index 000000000..90bcc5045 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/preview.spec.ts @@ -0,0 +1,81 @@ +jest.mock("@services/mongoose", () => ({ + AccountsRepository: jest.fn(), + WalletsRepository: jest.fn(), +})) + +import { previewPrimaryCashWalletCutover } from "@app/cash-wallet-cutover/preview" +import { WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" + +const account = ({ + id, + defaultWalletId, +}: { + id: string + defaultWalletId: string +}): Account => + ({ + id, + uuid: `${id}-uuid`, + defaultWalletId, + }) as Account + +const wallet = ({ id, currency }: { id: string; currency: WalletCurrency }): Wallet => + ({ + id, + type: WalletType.Checking, + currency, + }) as Wallet + +describe("preview primary cash wallet cutover", () => { + it("builds the preflight report and plan without repository writes", async () => { + const accounts = [ + account({ id: "account-1", defaultWalletId: "usd-1" }), + account({ id: "account-2", defaultWalletId: "usdt-2" }), + ] + + const accountsRepo = { + listUnlockedAccounts: function* () { + yield* accounts + }, + } + const walletsRepo = { + listByAccountId: jest.fn(async (accountId: AccountId) => { + if (accountId === "account-1") { + return [ + wallet({ id: "usd-1", currency: WalletCurrency.Usd }), + wallet({ id: "usdt-1", currency: WalletCurrency.Usdt }), + ] + } + + return [ + wallet({ id: "usd-2", currency: WalletCurrency.Usd }), + wallet({ id: "usdt-2", currency: WalletCurrency.Usdt }), + ] + }), + } + + const result = await previewPrimaryCashWalletCutover({ + cutoverVersion: 7, + runId: "run-7", + accountsRepo, + walletsRepo, + }) + + expect(result).toEqual({ + report: expect.objectContaining({ + totalAccounts: 2, + migrationCandidates: 1, + alreadyUsdt: 1, + canStart: true, + }), + plannedMigrations: [ + expect.objectContaining({ + accountId: "account-1", + legacyUsdWalletId: "usd-1", + destinationUsdtWalletId: "usdt-1", + }), + ], + }) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/runner.spec.ts b/test/flash/unit/app/cash-wallet-cutover/runner.spec.ts new file mode 100644 index 000000000..6987c3873 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/runner.spec.ts @@ -0,0 +1,126 @@ +import { CouldNotUpdateError } from "@domain/errors" + +import { runCashWalletMigrationBatch } from "@app/cash-wallet-cutover/runner" + +const migration = ( + status: CashWalletMigrationStatus, + id = `${status}-migration`, +): CashWalletMigration => ({ + id, + accountId: `${id}-account` as AccountId, + legacyUsdWalletId: `${id}-legacy-usd-wallet` as WalletId, + destinationUsdtWalletId: `${id}-usdt-wallet` as WalletId, + cutoverVersion: 7, + runId: "run-7", + status, + idempotencyKey: `cash-wallet-cutover:run-7:${id}-account`, + attempts: 0, + updatedAt: new Date("2026-05-20T00:00:00Z"), +}) + +describe("cash wallet migration batch runner", () => { + it("locks each runnable migration, executes one step, and releases the lock", async () => { + const runnable = [migration("not_started", "migration-1")] + const locked = { ...runnable[0], lockedBy: "worker-1" } + const completedStep = migration("started", "migration-1") + const migrationsRepo = { + listRunnableMigrations: jest.fn(async () => runnable), + acquireMigrationLock: jest.fn(async () => locked), + markMigrationFailed: jest.fn(), + releaseMigrationLock: jest.fn(async () => locked), + } + const executor = jest.fn(async () => completedStep) + + const result = await runCashWalletMigrationBatch({ + cutoverVersion: 7, + runId: "run-7", + workerId: "worker-1", + limit: 10, + lockStaleBefore: new Date("2026-05-20T15:00:00Z"), + migrationsRepo, + executor, + }) + + expect(result).toEqual({ attempted: 1, advanced: 1, failed: 0, skipped: 0 }) + expect(migrationsRepo.listRunnableMigrations).toHaveBeenCalledWith({ + cutoverVersion: 7, + runId: "run-7", + limit: 10, + }) + expect(migrationsRepo.acquireMigrationLock).toHaveBeenCalledWith({ + id: "migration-1", + workerId: "worker-1", + staleBefore: new Date("2026-05-20T15:00:00Z"), + cutoverVersion: 7, + runId: "run-7", + }) + expect(executor).toHaveBeenCalledWith(locked) + expect(migrationsRepo.releaseMigrationLock).toHaveBeenCalledWith({ + id: "migration-1", + workerId: "worker-1", + cutoverVersion: 7, + runId: "run-7", + }) + }) + + it("skips migrations that cannot be locked", async () => { + const lockError = new CouldNotUpdateError("lock unavailable") + const migrationsRepo = { + listRunnableMigrations: jest.fn(async () => [migration("started", "migration-1")]), + acquireMigrationLock: jest.fn(async () => lockError), + markMigrationFailed: jest.fn(), + releaseMigrationLock: jest.fn(), + } + const executor = jest.fn() + + const result = await runCashWalletMigrationBatch({ + cutoverVersion: 7, + runId: "run-7", + workerId: "worker-1", + limit: 10, + lockStaleBefore: new Date("2026-05-20T15:00:00Z"), + migrationsRepo, + executor, + }) + + expect(result).toEqual({ attempted: 1, advanced: 0, failed: 0, skipped: 1 }) + expect(executor).not.toHaveBeenCalled() + expect(migrationsRepo.releaseMigrationLock).not.toHaveBeenCalled() + }) + + it("releases the lock when execution fails", async () => { + const locked = migration("balance_move_sent", "migration-1") + const executionError = new CouldNotUpdateError("execution failed") + const migrationsRepo = { + listRunnableMigrations: jest.fn(async () => [locked]), + acquireMigrationLock: jest.fn(async () => locked), + markMigrationFailed: jest.fn(async () => ({ + ...locked, + status: "requires_operator_review" as const, + })), + releaseMigrationLock: jest.fn(async () => locked), + } + const executor = jest.fn(async () => executionError) + + const result = await runCashWalletMigrationBatch({ + cutoverVersion: 7, + runId: "run-7", + workerId: "worker-1", + limit: 10, + lockStaleBefore: new Date("2026-05-20T15:00:00Z"), + migrationsRepo, + executor, + }) + + expect(result).toEqual({ attempted: 1, advanced: 0, failed: 1, skipped: 0 }) + expect(migrationsRepo.markMigrationFailed).toHaveBeenCalledWith({ + id: "migration-1", + workerId: "worker-1", + cutoverVersion: 7, + runId: "run-7", + error: executionError, + status: "requires_operator_review", + }) + expect(migrationsRepo.releaseMigrationLock).not.toHaveBeenCalled() + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/runtime-services.spec.ts b/test/flash/unit/app/cash-wallet-cutover/runtime-services.spec.ts new file mode 100644 index 000000000..45e793e60 --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/runtime-services.spec.ts @@ -0,0 +1,222 @@ +import { CouldNotUpdateError } from "@domain/errors" +import { USDAmount, USDTAmount, WalletCurrency } from "@domain/shared" +import { WalletType } from "@domain/wallets" + +jest.mock("@app/accounts", () => ({ + addWalletIfNonexistent: jest.fn(), + updateDefaultWalletId: jest.fn(), +})) +jest.mock("@app/wallets", () => ({ + addInvoiceForRecipientForUsdWallet: jest.fn(), + addInvoiceNoAmountForRecipient: jest.fn(), + getBalanceForWallet: jest.fn(), +})) +jest.mock("@services/mongoose", () => ({ + AccountsRepository: jest.fn(() => ({ findById: jest.fn() })), +})) +jest.mock("@services/ibex/client", () => ({ + __esModule: true, + default: { + addInvoice: jest.fn(), + payInvoice: jest.fn(), + getTransactionDetails: jest.fn(), + }, +})) + +import { createCashWalletMigrationRuntimeServices } from "@app/cash-wallet-cutover/runtime-services" +import Ibex from "@services/ibex/client" + +const ibexAddInvoiceResponse = { + invoice: { + bolt11: + "lnbc140n1p3k6yzupp53p305l6de6s9xw2j0qaa59pl7lahys4f2uavwncll9z2vq0syvvsdqqcqzpgxqzuysp5mdgsaa734eg7srwx92rsn3hyc4xzt5tphfpadl5c6fanhppwaz4s9qyyssqm6yhnnhl8jltwjtclzk4g7nxr99ycsp4sqd6vksevqh06h8l3gm5fdhtl59t6g3fsalv26sj5zvwhxwlghc9wcfgkrjrtuh4873ejnspc5xksy", + }, +} + +const migration = (patch: Partial = {}): CashWalletMigration => ({ + id: "migration-id", + accountId: "account-id" as AccountId, + legacyUsdWalletId: "legacy-usd-wallet-id" as WalletId, + destinationUsdtWalletId: "usdt-wallet-id" as WalletId, + cutoverVersion: 7, + runId: "run-7", + status: "balance_move_verified", + idempotencyKey: "cash-wallet-cutover:run-7:account-id", + attempts: 0, + updatedAt: new Date("2026-05-20T00:00:00Z"), + ...patch, +}) + +describe("cash wallet migration runtime services", () => { + it("reads source USD balances as cents", async () => { + const deps = { + getBalanceForWallet: jest.fn(async () => USDAmount.cents("1234")), + } + + const services = createCashWalletMigrationRuntimeServices(deps) + + const result = await services.balanceReader.readSourceBalanceUsdCents(migration()) + + expect(result).toBe("1234") + expect(deps.getBalanceForWallet).toHaveBeenCalledWith({ + walletId: "legacy-usd-wallet-id", + currency: WalletCurrency.Usd, + }) + }) + + it("reads destination USDT balances as micros", async () => { + const deps = { + getBalanceForWallet: jest.fn(async () => USDTAmount.smallestUnits("5000000")), + } + + const services = createCashWalletMigrationRuntimeServices(deps) + + const result = + await services.balanceReader.readDestinationBalanceUsdtMicros(migration()) + + expect(result).toBe("5000000") + expect(deps.getBalanceForWallet).toHaveBeenCalledWith({ + walletId: "usdt-wallet-id", + currency: WalletCurrency.Usdt, + }) + }) + + it("ensures the expected destination USDT wallet exists", async () => { + const deps = { + addWalletIfNonexistent: jest.fn(async () => ({ + id: "usdt-wallet-id" as WalletId, + })), + } + + const services = createCashWalletMigrationRuntimeServices(deps) + + const result = await services.provisioningService.ensureDestinationWallet({ + accountId: "account-id" as AccountId, + destinationUsdtWalletId: "usdt-wallet-id" as WalletId, + }) + + expect(result).toBe(true) + expect(deps.addWalletIfNonexistent).toHaveBeenCalledWith({ + accountId: "account-id", + type: WalletType.Checking, + currency: WalletCurrency.Usdt, + }) + }) + + it("creates no-amount destination invoices through IBEX", async () => { + jest.mocked(Ibex.addInvoice).mockResolvedValue(ibexAddInvoiceResponse as never) + + const services = createCashWalletMigrationRuntimeServices() + + const result = await services.invoiceService.createNoAmountInvoice({ + recipientWalletId: "usdt-wallet-id" as WalletId, + memo: "cash-wallet-cutover:run-7:migration-id:balance-move", + }) + + expect(result).toMatchObject({ + paymentRequest: ibexAddInvoiceResponse.invoice.bolt11, + }) + expect(Ibex.addInvoice).toHaveBeenCalledWith({ + accountId: "usdt-wallet-id", + memo: "cash-wallet-cutover:run-7:migration-id:balance-move", + expiration: 900, + }) + }) + + it("extracts the IBEX transaction id after paying an invoice with a sender-side USD cap", async () => { + const deps = { + payInvoice: jest.fn(async () => ({ + transaction: { id: "ibex-tx-id" }, + })), + } + + const services = createCashWalletMigrationRuntimeServices(deps) + + const result = await services.paymentService.payInvoice({ + senderWalletId: "legacy-usd-wallet-id" as WalletId, + paymentRequest: "lnbc1payment", + senderAmountUsdCents: "1000", + }) + + expect(result).toEqual({ transactionId: "ibex-tx-id" }) + const paymentArgs = deps.payInvoice.mock.calls[0][0]! + expect(paymentArgs.accountId).toBe("legacy-usd-wallet-id") + expect(paymentArgs.invoice).toBe("lnbc1payment") + expect(paymentArgs.send).toBeInstanceOf(USDAmount) + expect(paymentArgs.send.asCents()).toBe("1000") + }) + + it("returns an error when IBEX payment response has no transaction id", async () => { + const services = createCashWalletMigrationRuntimeServices({ + payInvoice: jest.fn(async () => ({})), + }) + + const result = await services.paymentService.payInvoice({ + senderWalletId: "legacy-usd-wallet-id" as WalletId, + paymentRequest: "lnbc1payment", + }) + + expect(result).toBeInstanceOf(Error) + }) + + it("computes the fee reimbursement as the exact destination USDT shortfall", async () => { + const deps = { + getBalanceForWallet: jest.fn(async () => USDTAmount.smallestUnits("14930000")), + } + + const services = createCashWalletMigrationRuntimeServices(deps) + + const result = await services.feeService.readFeeAmountUsdtMicros( + migration({ + balanceMovePaymentTransactionId: "ibex-tx-id", + destinationAmountUsdtMicros: "10000000", + destinationStartingBalanceUsdtMicros: "5000000", + }), + ) + + expect(result).toBe("70000") + expect(deps.getBalanceForWallet).toHaveBeenCalledWith({ + walletId: "usdt-wallet-id", + currency: WalletCurrency.Usdt, + }) + }) + + it("flips the default wallet and returns the previous default wallet id", async () => { + const deps = { + accountsRepo: { + findById: jest.fn(async () => ({ + defaultWalletId: "legacy-usd-wallet-id" as WalletId, + })), + }, + updateDefaultWalletId: jest.fn(async () => ({ defaultWalletId: "usdt-wallet-id" })), + } + + const services = createCashWalletMigrationRuntimeServices(deps) + + const result = await services.pointerService.flipDefaultWallet({ + accountId: "account-id" as AccountId, + destinationWalletId: "usdt-wallet-id" as WalletId, + }) + + expect(result).toEqual({ + previousDefaultWalletId: "legacy-usd-wallet-id", + }) + expect(deps.updateDefaultWalletId).toHaveBeenCalledWith({ + accountId: "account-id", + walletId: "usdt-wallet-id", + }) + }) + + it("propagates legacy zero verification errors", async () => { + const error = new CouldNotUpdateError("balance lookup failed") + const services = createCashWalletMigrationRuntimeServices({ + getBalanceForWallet: jest.fn(async () => error), + }) + + const result = await services.legacyWalletVerifier.verifyLegacyWalletZero({ + legacyUsdWalletId: "legacy-usd-wallet-id" as WalletId, + }) + + expect(result).toBe(error) + }) +}) diff --git a/test/flash/unit/app/cash-wallet-cutover/worker.spec.ts b/test/flash/unit/app/cash-wallet-cutover/worker.spec.ts new file mode 100644 index 000000000..0d27c14ae --- /dev/null +++ b/test/flash/unit/app/cash-wallet-cutover/worker.spec.ts @@ -0,0 +1,863 @@ +import { CouldNotUpdateError } from "@domain/errors" + +import { + createCashWalletMigrationBalanceMoveInvoice, + createCashWalletMigrationFeeReimbursementInvoice, + flipCashWalletMigrationDefaultPointer, + completeCashWalletMigration, + markCashWalletMigrationFeeReimbursed, + markCashWalletMigrationBalanceMoveSent, + provisionCashWalletMigrationDestination, + recordCashWalletMigrationBalance, + sendCashWalletMigrationBalanceMovePayment, + sendCashWalletMigrationFeeReimbursementPayment, + skipCashWalletMigrationFeeReimbursement, + startCashWalletMigration, + verifyCashWalletMigrationBalanceMove, + verifyCashWalletMigrationLegacyZero, +} from "@app/cash-wallet-cutover/worker" + +const migration = (status: CashWalletMigrationStatus): CashWalletMigration => ({ + id: "migration-id", + accountId: "account-id" as AccountId, + legacyUsdWalletId: "legacy-usd-wallet-id" as WalletId, + destinationUsdtWalletId: "usdt-wallet-id" as WalletId, + cutoverVersion: 7, + runId: "run-7", + status, + idempotencyKey: "cash-wallet-cutover:run-7:account-id", + attempts: 0, + updatedAt: new Date("2026-05-20T00:00:00Z"), +}) + +describe("cash wallet migration worker checkpoints", () => { + it("starts a not-started migration with an atomic repository transition", async () => { + const startedAt = new Date("2026-05-20T13:00:00Z") + const migrationsRepo = { + transitionMigration: jest.fn(async () => migration("started")), + } + + const result = await startCashWalletMigration({ + migration: migration("not_started"), + migrationsRepo, + startedAt, + }) + + expect(result).toMatchObject({ status: "started" }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "not_started", + to: "started", + cutoverVersion: 7, + runId: "run-7", + patch: { startedAt }, + }) + }) + + it("rejects invalid start transitions before touching the repository", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + + const result = await startCashWalletMigration({ + migration: migration("balance_read"), + migrationsRepo, + startedAt: new Date("2026-05-20T13:00:00Z"), + }) + + expect(result).toBeInstanceOf(Error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("returns repository transition failures", async () => { + const error = new CouldNotUpdateError("transition failed") + const migrationsRepo = { + transitionMigration: jest.fn(async () => error), + } + + const result = await startCashWalletMigration({ + migration: migration("not_started"), + migrationsRepo, + startedAt: new Date("2026-05-20T13:00:00Z"), + }) + + expect(result).toBe(error) + }) + + it("provisions the destination wallet before reading balances", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => migration("provisioned")), + } + const provisioningService = { + ensureDestinationWallet: jest.fn(async () => true as const), + } + + const result = await provisionCashWalletMigrationDestination({ + migration: migration("started"), + provisioningService, + migrationsRepo, + }) + + expect(result).toMatchObject({ status: "provisioned" }) + expect(provisioningService.ensureDestinationWallet).toHaveBeenCalledWith({ + accountId: "account-id", + destinationUsdtWalletId: "usdt-wallet-id", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "started", + to: "provisioned", + cutoverVersion: 7, + runId: "run-7", + }) + }) + + it("returns destination wallet provisioning failures without advancing", async () => { + const error = new CouldNotUpdateError("destination wallet missing") + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const provisioningService = { + ensureDestinationWallet: jest.fn(async () => error), + } + + const result = await provisionCashWalletMigrationDestination({ + migration: migration("started"), + provisioningService, + migrationsRepo, + }) + + expect(result).toBe(error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("records source balance and destination amount before creating invoices", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("balance_read"), + sourceBalanceUsdCents: "1234", + destinationAmountUsdtMicros: "12340000", + destinationStartingBalanceUsdtMicros: "5000000", + })), + } + + const result = await recordCashWalletMigrationBalance({ + migration: migration("provisioned"), + migrationsRepo, + sourceBalanceUsdCents: "1234", + destinationStartingBalanceUsdtMicros: "5000000", + }) + + expect(result).toMatchObject({ + status: "balance_read", + sourceBalanceUsdCents: "1234", + destinationAmountUsdtMicros: "12340000", + destinationStartingBalanceUsdtMicros: "5000000", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "provisioned", + to: "balance_read", + cutoverVersion: 7, + runId: "run-7", + patch: { + sourceBalanceUsdCents: "1234", + destinationAmountUsdtMicros: "12340000", + destinationStartingBalanceUsdtMicros: "5000000", + }, + }) + }) + + it("rejects invalid balance amounts before touching the repository", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + + const result = await recordCashWalletMigrationBalance({ + migration: migration("provisioned"), + migrationsRepo, + sourceBalanceUsdCents: "12.34", + destinationStartingBalanceUsdtMicros: "0", + }) + + expect(result).toBeInstanceOf(Error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("creates a no-amount balance move invoice on the destination wallet", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("invoice_created"), + balanceMoveInvoicePaymentRequest: "lnbc1balance-move", + balanceMoveInvoicePaymentHash: "paymentHash", + })), + } + const invoice = { + paymentRequest: "lnbc1balance-move" as EncodedPaymentRequest, + paymentHash: "paymentHash" as PaymentHash, + } as LnInvoice + const invoiceService = { + createNoAmountInvoice: jest.fn(async () => invoice), + } + + const result = await createCashWalletMigrationBalanceMoveInvoice({ + migration: { + ...migration("balance_read"), + destinationAmountUsdtMicros: "12340000", + }, + invoiceService, + migrationsRepo, + }) + + expect(result).toMatchObject({ + status: "invoice_created", + balanceMoveInvoicePaymentRequest: "lnbc1balance-move", + balanceMoveInvoicePaymentHash: "paymentHash", + }) + expect(invoiceService.createNoAmountInvoice).toHaveBeenCalledWith({ + recipientWalletId: "usdt-wallet-id", + memo: "cash-wallet-cutover:run-7:migration-id:balance-move", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "balance_read", + to: "invoice_created", + cutoverVersion: 7, + runId: "run-7", + patch: { + balanceMoveInvoicePaymentRequest: "lnbc1balance-move", + balanceMoveInvoicePaymentHash: "paymentHash", + }, + }) + }) + + it("rejects balance move invoice creation when the destination amount is missing", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const invoiceService = { + createNoAmountInvoice: jest.fn(), + } + + const result = await createCashWalletMigrationBalanceMoveInvoice({ + migration: migration("balance_read"), + invoiceService, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(Error) + expect(invoiceService.createNoAmountInvoice).not.toHaveBeenCalled() + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("returns balance move invoice creation failures without advancing the checkpoint", async () => { + const error = new CouldNotUpdateError("invoice creation failed") + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const invoiceService = { + createNoAmountInvoice: jest.fn(async () => error), + } + + const result = await createCashWalletMigrationBalanceMoveInvoice({ + migration: { + ...migration("balance_read"), + destinationAmountUsdtMicros: "12340000", + }, + invoiceService, + migrationsRepo, + }) + + expect(result).toBe(error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("sends the balance move payment from the legacy wallet capped to the recorded source balance", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("balance_move_sending"), + balanceMovePaymentTransactionId: "ibex-tx-id", + })), + } + const paymentService = { + payInvoice: jest.fn(async () => ({ + transactionId: "ibex-tx-id" as IbexTransactionId, + })), + } + + const result = await sendCashWalletMigrationBalanceMovePayment({ + migration: { + ...migration("invoice_created"), + balanceMoveInvoicePaymentRequest: "lnbc1balance-move", + sourceBalanceUsdCents: "1000", + }, + paymentService, + migrationsRepo, + }) + + expect(result).toMatchObject({ + status: "balance_move_sending", + balanceMovePaymentTransactionId: "ibex-tx-id", + }) + expect(paymentService.payInvoice).toHaveBeenCalledWith({ + senderWalletId: "legacy-usd-wallet-id", + paymentRequest: "lnbc1balance-move", + senderAmountUsdCents: "1000", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "invoice_created", + to: "balance_move_sending", + cutoverVersion: 7, + runId: "run-7", + patch: { + balanceMovePaymentTransactionId: "ibex-tx-id", + }, + }) + }) + + it("rejects balance move payment sending when the invoice payment request is missing", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const paymentService = { + payInvoice: jest.fn(), + } + + const result = await sendCashWalletMigrationBalanceMovePayment({ + migration: migration("invoice_created"), + paymentService, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(Error) + expect(paymentService.payInvoice).not.toHaveBeenCalled() + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("rejects balance move payment sending when the source balance is missing", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const paymentService = { + payInvoice: jest.fn(), + } + + const result = await sendCashWalletMigrationBalanceMovePayment({ + migration: { + ...migration("invoice_created"), + balanceMoveInvoicePaymentRequest: "lnbc1balance-move", + }, + paymentService, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(Error) + expect(paymentService.payInvoice).not.toHaveBeenCalled() + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("marks the balance move payment as sent after a transaction id is recorded", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("balance_move_sent"), + balanceMovePaymentTransactionId: "ibex-tx-id", + })), + } + + const result = await markCashWalletMigrationBalanceMoveSent({ + migration: { + ...migration("balance_move_sending"), + balanceMovePaymentTransactionId: "ibex-tx-id", + }, + migrationsRepo, + }) + + expect(result).toMatchObject({ + status: "balance_move_sent", + balanceMovePaymentTransactionId: "ibex-tx-id", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "balance_move_sending", + to: "balance_move_sent", + cutoverVersion: 7, + runId: "run-7", + }) + }) + + it("rejects marking the balance move payment as sent before a transaction id exists", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + + const result = await markCashWalletMigrationBalanceMoveSent({ + migration: migration("balance_move_sending"), + migrationsRepo, + }) + + expect(result).toBeInstanceOf(Error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("verifies the balance move before fee reimbursement", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => migration("balance_move_verified")), + } + const balanceVerifier = { + verifyBalanceMove: jest.fn(async () => true as const), + } + + const result = await verifyCashWalletMigrationBalanceMove({ + migration: { + ...migration("balance_move_sent"), + balanceMovePaymentTransactionId: "ibex-tx-id", + }, + balanceVerifier, + migrationsRepo, + }) + + expect(result).toMatchObject({ status: "balance_move_verified" }) + expect(balanceVerifier.verifyBalanceMove).toHaveBeenCalledWith({ + legacyUsdWalletId: "legacy-usd-wallet-id", + destinationUsdtWalletId: "usdt-wallet-id", + sourceBalanceUsdCents: undefined, + destinationAmountUsdtMicros: undefined, + transactionId: "ibex-tx-id", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "balance_move_sent", + to: "balance_move_verified", + cutoverVersion: 7, + runId: "run-7", + }) + }) + + it("returns balance move verification failures without advancing the checkpoint", async () => { + const error = new CouldNotUpdateError("balance move not settled") + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const balanceVerifier = { + verifyBalanceMove: jest.fn(async () => error), + } + + const result = await verifyCashWalletMigrationBalanceMove({ + migration: { + ...migration("balance_move_sent"), + balanceMovePaymentTransactionId: "ibex-tx-id", + }, + balanceVerifier, + migrationsRepo, + }) + + expect(result).toBe(error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("rejects balance move verification before a transaction id exists", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const balanceVerifier = { + verifyBalanceMove: jest.fn(), + } + + const result = await verifyCashWalletMigrationBalanceMove({ + migration: migration("balance_move_sent"), + balanceVerifier, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(Error) + expect(balanceVerifier.verifyBalanceMove).not.toHaveBeenCalled() + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("creates a fee reimbursement invoice on the destination wallet for the exact USDT shortfall", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("fee_reimbursement_invoice_created"), + feeAmountUsdCents: "8", + feeAmountUsdtMicros: "70001", + feeReimbursementInvoicePaymentRequest: "lnbc1fee-reimbursement", + feeReimbursementInvoicePaymentHash: "feePaymentHash", + })), + } + const invoice = { + paymentRequest: "lnbc1fee-reimbursement" as EncodedPaymentRequest, + paymentHash: "feePaymentHash" as PaymentHash, + } as LnInvoice + const invoiceService = { + createInvoice: jest.fn(async () => invoice), + } + + const result = await createCashWalletMigrationFeeReimbursementInvoice({ + migration: migration("balance_move_verified"), + invoiceService, + migrationsRepo, + feeAmountUsdtMicros: "70001", + }) + + expect(result).toMatchObject({ + status: "fee_reimbursement_invoice_created", + feeAmountUsdCents: "8", + feeAmountUsdtMicros: "70001", + feeReimbursementInvoicePaymentRequest: "lnbc1fee-reimbursement", + feeReimbursementInvoicePaymentHash: "feePaymentHash", + }) + expect(invoiceService.createInvoice).toHaveBeenCalledWith({ + recipientWalletId: "usdt-wallet-id", + amount: "70001", + memo: "cash-wallet-cutover:run-7:migration-id:fee-reimbursement", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "balance_move_verified", + to: "fee_reimbursement_invoice_created", + cutoverVersion: 7, + runId: "run-7", + patch: { + feeAmountUsdCents: "8", + feeAmountUsdtMicros: "70001", + feeReimbursementInvoicePaymentRequest: "lnbc1fee-reimbursement", + feeReimbursementInvoicePaymentHash: "feePaymentHash", + }, + }) + }) + + it("rejects invalid fee reimbursement amounts before creating an invoice", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const invoiceService = { + createInvoice: jest.fn(), + } + + const result = await createCashWalletMigrationFeeReimbursementInvoice({ + migration: migration("balance_move_verified"), + invoiceService, + migrationsRepo, + feeAmountUsdtMicros: "0.07", + }) + + expect(result).toBeInstanceOf(Error) + expect(invoiceService.createInvoice).not.toHaveBeenCalled() + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("returns fee reimbursement invoice creation failures without advancing", async () => { + const error = new CouldNotUpdateError("fee invoice failed") + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const invoiceService = { + createInvoice: jest.fn(async () => error), + } + + const result = await createCashWalletMigrationFeeReimbursementInvoice({ + migration: migration("balance_move_verified"), + invoiceService, + migrationsRepo, + feeAmountUsdtMicros: "70000", + }) + + expect(result).toBe(error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("skips fee reimbursement when there is no destination shortfall", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("fee_reimbursed"), + feeAmountUsdCents: "0", + feeAmountUsdtMicros: "0", + })), + } + + const result = await skipCashWalletMigrationFeeReimbursement({ + migration: migration("balance_move_verified"), + migrationsRepo, + }) + + expect(result).toMatchObject({ + status: "fee_reimbursed", + feeAmountUsdCents: "0", + feeAmountUsdtMicros: "0", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "balance_move_verified", + to: "fee_reimbursed", + cutoverVersion: 7, + runId: "run-7", + patch: { + feeAmountUsdCents: "0", + feeAmountUsdtMicros: "0", + }, + }) + }) + + it("sends the fee reimbursement payment from the treasury wallet", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("fee_reimbursement_sending"), + feeReimbursementPaymentTransactionId: "fee-ibex-tx-id", + })), + } + const paymentService = { + payInvoice: jest.fn(async () => ({ + transactionId: "fee-ibex-tx-id" as IbexTransactionId, + })), + } + + const result = await sendCashWalletMigrationFeeReimbursementPayment({ + migration: { + ...migration("fee_reimbursement_invoice_created"), + feeReimbursementInvoicePaymentRequest: "lnbc1fee-reimbursement", + }, + treasuryWalletId: "treasury-wallet-id" as WalletId, + paymentService, + migrationsRepo, + }) + + expect(result).toMatchObject({ + status: "fee_reimbursement_sending", + feeReimbursementPaymentTransactionId: "fee-ibex-tx-id", + }) + expect(paymentService.payInvoice).toHaveBeenCalledWith({ + senderWalletId: "treasury-wallet-id", + paymentRequest: "lnbc1fee-reimbursement", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "fee_reimbursement_invoice_created", + to: "fee_reimbursement_sending", + cutoverVersion: 7, + runId: "run-7", + patch: { + feeReimbursementPaymentTransactionId: "fee-ibex-tx-id", + }, + }) + }) + + it("rejects fee reimbursement sending when the invoice payment request is missing", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const paymentService = { + payInvoice: jest.fn(), + } + + const result = await sendCashWalletMigrationFeeReimbursementPayment({ + migration: migration("fee_reimbursement_invoice_created"), + treasuryWalletId: "treasury-wallet-id" as WalletId, + paymentService, + migrationsRepo, + }) + + expect(result).toBeInstanceOf(Error) + expect(paymentService.payInvoice).not.toHaveBeenCalled() + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("returns fee reimbursement payment failures without advancing", async () => { + const error = new CouldNotUpdateError("fee payment failed") + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const paymentService = { + payInvoice: jest.fn(async () => error), + } + + const result = await sendCashWalletMigrationFeeReimbursementPayment({ + migration: { + ...migration("fee_reimbursement_invoice_created"), + feeReimbursementInvoicePaymentRequest: "lnbc1fee-reimbursement", + }, + treasuryWalletId: "treasury-wallet-id" as WalletId, + paymentService, + migrationsRepo, + }) + + expect(result).toBe(error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("marks the fee reimbursement as complete after a transaction id is recorded", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("fee_reimbursed"), + feeReimbursementPaymentTransactionId: "fee-ibex-tx-id", + })), + } + + const result = await markCashWalletMigrationFeeReimbursed({ + migration: { + ...migration("fee_reimbursement_sending"), + feeReimbursementPaymentTransactionId: "fee-ibex-tx-id", + }, + migrationsRepo, + }) + + expect(result).toMatchObject({ + status: "fee_reimbursed", + feeReimbursementPaymentTransactionId: "fee-ibex-tx-id", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "fee_reimbursement_sending", + to: "fee_reimbursed", + cutoverVersion: 7, + runId: "run-7", + }) + }) + + it("rejects marking fee reimbursement complete before a transaction id exists", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(), + } + + const result = await markCashWalletMigrationFeeReimbursed({ + migration: migration("fee_reimbursement_sending"), + migrationsRepo, + }) + + expect(result).toBeInstanceOf(Error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("flips the account default pointer to the destination USDT wallet", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("pointer_flipped"), + previousDefaultWalletId: "legacy-usd-wallet-id" as WalletId, + })), + } + const pointerService = { + flipDefaultWallet: jest.fn(async () => ({ + previousDefaultWalletId: "legacy-usd-wallet-id" as WalletId, + })), + } + + const result = await flipCashWalletMigrationDefaultPointer({ + migration: migration("fee_reimbursed"), + pointerService, + migrationsRepo, + }) + + expect(result).toMatchObject({ + status: "pointer_flipped", + previousDefaultWalletId: "legacy-usd-wallet-id", + }) + expect(pointerService.flipDefaultWallet).toHaveBeenCalledWith({ + accountId: "account-id", + destinationWalletId: "usdt-wallet-id", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "fee_reimbursed", + to: "pointer_flipped", + cutoverVersion: 7, + runId: "run-7", + patch: { + previousDefaultWalletId: "legacy-usd-wallet-id", + }, + }) + }) + + it("returns pointer flip failures without advancing", async () => { + const error = new CouldNotUpdateError("default wallet update failed") + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const pointerService = { + flipDefaultWallet: jest.fn(async () => error), + } + + const result = await flipCashWalletMigrationDefaultPointer({ + migration: migration("fee_reimbursed"), + pointerService, + migrationsRepo, + }) + + expect(result).toBe(error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("verifies the legacy USD wallet is zero after the pointer flip", async () => { + const migrationsRepo = { + transitionMigration: jest.fn(async () => migration("legacy_zero_verified")), + } + const legacyWalletVerifier = { + verifyLegacyWalletZero: jest.fn(async () => true as const), + } + + const result = await verifyCashWalletMigrationLegacyZero({ + migration: migration("pointer_flipped"), + legacyWalletVerifier, + migrationsRepo, + }) + + expect(result).toMatchObject({ status: "legacy_zero_verified" }) + expect(legacyWalletVerifier.verifyLegacyWalletZero).toHaveBeenCalledWith({ + legacyUsdWalletId: "legacy-usd-wallet-id", + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "pointer_flipped", + to: "legacy_zero_verified", + cutoverVersion: 7, + runId: "run-7", + }) + }) + + it("returns legacy zero verification failures without advancing", async () => { + const error = new CouldNotUpdateError("legacy wallet still has a balance") + const migrationsRepo = { + transitionMigration: jest.fn(), + } + const legacyWalletVerifier = { + verifyLegacyWalletZero: jest.fn(async () => error), + } + + const result = await verifyCashWalletMigrationLegacyZero({ + migration: migration("pointer_flipped"), + legacyWalletVerifier, + migrationsRepo, + }) + + expect(result).toBe(error) + expect(migrationsRepo.transitionMigration).not.toHaveBeenCalled() + }) + + it("completes the migration after legacy zero verification", async () => { + const completedAt = new Date("2026-05-20T15:30:00Z") + const migrationsRepo = { + transitionMigration: jest.fn(async () => ({ + ...migration("complete"), + completedAt, + })), + } + + const result = await completeCashWalletMigration({ + migration: migration("legacy_zero_verified"), + migrationsRepo, + completedAt, + }) + + expect(result).toMatchObject({ + status: "complete", + completedAt, + }) + expect(migrationsRepo.transitionMigration).toHaveBeenCalledWith({ + id: "migration-id", + from: "legacy_zero_verified", + to: "complete", + cutoverVersion: 7, + runId: "run-7", + patch: { completedAt }, + }) + }) +}) diff --git a/test/flash/unit/graphql/cash-wallet-cutover.spec.ts b/test/flash/unit/graphql/cash-wallet-cutover.spec.ts new file mode 100644 index 000000000..f8e102a1e --- /dev/null +++ b/test/flash/unit/graphql/cash-wallet-cutover.spec.ts @@ -0,0 +1,81 @@ +jest.mock("@services/mongoose/cash-wallet-cutover", () => ({ + CashWalletCutoverRepository: jest.fn(), +})) + +import CashWalletCutoverQuery from "@graphql/shared/root/query/cash-wallet-cutover" +import CashWalletCutoverUpdateMutation from "@graphql/admin/root/mutation/cash-wallet-cutover-update" +import { CashWalletCutoverRepository } from "@services/mongoose/cash-wallet-cutover" + +describe("cash wallet cutover GraphQL surface", () => { + const updatedAt = new Date("2026-05-22T12:00:00Z") + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("returns the public cutover flag state", async () => { + const getConfig = jest.fn(async () => ({ + state: "in_progress" as const, + cutoverVersion: 7, + runId: "run-7", + scheduledAt: new Date("2026-05-22T13:00:00Z"), + updatedAt, + })) + jest.mocked(CashWalletCutoverRepository).mockReturnValue({ getConfig } as never) + + const result = await CashWalletCutoverQuery.resolve?.( + null, + {}, + {} as GraphQLPublicContext, + {} as never, + ) + + expect(result).toMatchObject({ + state: "in_progress", + cutoverVersion: 7, + runId: "run-7", + scheduledAt: new Date("2026-05-22T13:00:00Z"), + }) + }) + + it("lets admins mutate the cutover flag", async () => { + const updateConfig = jest.fn(async () => ({ + state: "in_progress" as const, + cutoverVersion: 8, + runId: "run-8", + updatedBy: "admin-user-id", + updatedAt, + })) + jest.mocked(CashWalletCutoverRepository).mockReturnValue({ updateConfig } as never) + + const result = await CashWalletCutoverUpdateMutation.resolve?.( + null, + { + input: { + state: "in_progress", + cutoverVersion: 8, + runId: "run-8", + }, + }, + { user: { id: "admin-user-id" } } as GraphQLAdminContext, + {} as never, + ) + + expect(updateConfig).toHaveBeenCalledWith( + { + state: "in_progress", + cutoverVersion: 8, + runId: "run-8", + }, + "admin-user-id", + ) + expect(result).toMatchObject({ + errors: [], + cashWalletCutover: { + state: "in_progress", + cutoverVersion: 8, + runId: "run-8", + }, + }) + }) +}) diff --git a/test/flash/unit/graphql/shared/types/object/usd-wallet.spec.ts b/test/flash/unit/graphql/shared/types/object/usd-wallet.spec.ts new file mode 100644 index 000000000..272bd40d6 --- /dev/null +++ b/test/flash/unit/graphql/shared/types/object/usd-wallet.spec.ts @@ -0,0 +1,17 @@ +import { usdtMicrosToUsdCents } from "@graphql/shared/types/object/usd-wallet" + +describe("UsdWallet legacy compatibility balance", () => { + it("converts USDT micros to USD cents from integer smallest units", () => { + expect(usdtMicrosToUsdCents("10000000")).toBe(1000) + }) + + it("accepts formatted USDT smallest units from precision calls", () => { + expect(usdtMicrosToUsdCents("10000000.00000000")).toBe(1000) + }) + + it("rejects non-zero fractional micros", () => { + expect(() => usdtMicrosToUsdCents("10000000.5")).toThrow( + "Cannot convert fractional USDT micros", + ) + }) +}) diff --git a/test/flash/unit/services/mongoose/cash-wallet-cutover.spec.ts b/test/flash/unit/services/mongoose/cash-wallet-cutover.spec.ts new file mode 100644 index 000000000..fcb76e7de --- /dev/null +++ b/test/flash/unit/services/mongoose/cash-wallet-cutover.spec.ts @@ -0,0 +1,242 @@ +import { CouldNotUpdateError } from "@domain/errors" +import { CashWalletCutoverRepository } from "@services/mongoose/cash-wallet-cutover" +import { CashWalletCutoverConfig, CashWalletMigration } from "@services/mongoose/schema" + +jest.mock("@services/mongoose/schema", () => ({ + CashWalletCutoverConfig: { + findById: jest.fn(), + findOneAndUpdate: jest.fn(), + }, + CashWalletMigration: { + findOne: jest.fn(), + findOneAndUpdate: jest.fn(), + find: jest.fn(), + countDocuments: jest.fn(), + }, +})) + +describe("CashWalletCutoverRepository", () => { + const repo = CashWalletCutoverRepository() + const updatedAt = new Date("2026-05-19T00:00:00Z") + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("returns default pre state when no config exists", async () => { + jest.mocked(CashWalletCutoverConfig.findById).mockResolvedValue(null as never) + + const result = await repo.getConfig() + + expect(result).toEqual({ + state: "pre", + cutoverVersion: 1, + updatedAt: new Date(0), + }) + }) + + it("upserts singleton config", async () => { + jest.mocked(CashWalletCutoverConfig.findOneAndUpdate).mockResolvedValue({ + _id: "cash_wallet_cutover", + state: "in_progress", + cutoverVersion: 2, + runId: "run-2", + updatedBy: "operator", + updatedAt, + } as never) + + const result = await repo.updateConfig( + { state: "in_progress", cutoverVersion: 2, runId: "run-2" }, + "operator", + ) + + expect(CashWalletCutoverConfig.findOneAndUpdate).toHaveBeenCalledWith( + { _id: "cash_wallet_cutover" }, + expect.objectContaining({ + $set: expect.objectContaining({ + state: "in_progress", + cutoverVersion: 2, + runId: "run-2", + updatedBy: "operator", + }), + }), + { upsert: true, new: true }, + ) + expect(result).toMatchObject({ + state: "in_progress", + cutoverVersion: 2, + runId: "run-2", + }) + }) + + it("creates one migration record per account id and run", async () => { + jest.mocked(CashWalletMigration.findOneAndUpdate).mockResolvedValue({ + _id: "migration-id", + accountId: "account-id", + legacyUsdWalletId: "usd-wallet-id", + destinationUsdtWalletId: "usdt-wallet-id", + cutoverVersion: 2, + runId: "run-2", + status: "not_started", + idempotencyKey: "run-2:account-id", + attempts: 0, + updatedAt, + } as never) + + const result = await repo.upsertMigration({ + accountId: "account-id" as AccountId, + legacyUsdWalletId: "usd-wallet-id" as WalletId, + destinationUsdtWalletId: "usdt-wallet-id" as WalletId, + cutoverVersion: 2, + runId: "run-2", + idempotencyKey: "run-2:account-id", + }) + + expect(CashWalletMigration.findOneAndUpdate).toHaveBeenCalledWith( + { accountId: "account-id", runId: "run-2" }, + expect.objectContaining({ + $setOnInsert: expect.objectContaining({ + accountId: "account-id", + runId: "run-2", + status: "not_started", + }), + }), + { upsert: true, new: true }, + ) + expect(result).toMatchObject({ id: "migration-id", accountId: "account-id" }) + }) + + it("transitions migration status atomically", async () => { + jest.mocked(CashWalletMigration.findOneAndUpdate).mockResolvedValue({ + _id: "migration-id", + accountId: "account-id", + legacyUsdWalletId: "usd-wallet-id", + destinationUsdtWalletId: "usdt-wallet-id", + cutoverVersion: 2, + runId: "run-2", + status: "started", + idempotencyKey: "run-2:account-id", + attempts: 0, + updatedAt, + } as never) + + const result = await repo.transitionMigration({ + id: "migration-id", + from: "not_started", + to: "started", + cutoverVersion: 2, + runId: "run-2", + patch: { startedAt: updatedAt }, + }) + + expect(CashWalletMigration.findOneAndUpdate).toHaveBeenCalledWith( + { _id: "migration-id", status: "not_started", cutoverVersion: 2, runId: "run-2" }, + expect.objectContaining({ $set: expect.objectContaining({ status: "started" }) }), + { new: true }, + ) + expect(result).toMatchObject({ status: "started" }) + }) + + it("acquires and rejects active locks atomically", async () => { + const staleBefore = new Date("2026-05-19T00:00:00Z") + jest.mocked(CashWalletMigration.findOneAndUpdate).mockResolvedValue(null as never) + + const result = await repo.acquireMigrationLock({ + id: "migration-id", + workerId: "worker-1", + staleBefore, + cutoverVersion: 2, + runId: "run-2", + }) + + expect(CashWalletMigration.findOneAndUpdate).toHaveBeenCalledWith( + { + _id: "migration-id", + cutoverVersion: 2, + runId: "run-2", + $or: [{ lockedAt: null }, { lockedAt: { $lt: staleBefore } }], + }, + expect.objectContaining({ + $set: expect.objectContaining({ lockedBy: "worker-1" }), + }), + { new: true }, + ) + expect(result).toBeInstanceOf(Error) + }) + + it("finds resumable non-terminal migrations for the current run", async () => { + const limit = jest.fn().mockResolvedValue([]) + const sort = jest.fn(() => ({ limit })) + jest.mocked(CashWalletMigration.find).mockReturnValue({ sort } as never) + + const result = await repo.listRunnableMigrations({ + cutoverVersion: 2, + runId: "run-2", + limit: 10, + }) + + expect(CashWalletMigration.find).toHaveBeenCalledWith( + expect.objectContaining({ + cutoverVersion: 2, + runId: "run-2", + status: { + $nin: expect.arrayContaining([ + "complete", + "failed", + "requires_operator_review", + ]), + }, + }), + ) + expect(sort).toHaveBeenCalledWith({ updatedAt: 1 }) + expect(limit).toHaveBeenCalledWith(10) + expect(result).toEqual([]) + }) + + it("marks migration failures durably and clears the worker lock", async () => { + jest.mocked(CashWalletMigration.findOneAndUpdate).mockResolvedValue({ + _id: "migration-id", + accountId: "account-id", + legacyUsdWalletId: "usd-wallet-id", + destinationUsdtWalletId: "usdt-wallet-id", + cutoverVersion: 2, + runId: "run-2", + status: "requires_operator_review", + idempotencyKey: "run-2:account-id", + attempts: 2, + lastError: "execution failed", + lockedAt: null, + lockedBy: null, + updatedAt, + } as never) + + const error = new CouldNotUpdateError("execution failed") + const result = await repo.markMigrationFailed({ + id: "migration-id", + workerId: "worker-1", + cutoverVersion: 2, + runId: "run-2", + status: "requires_operator_review", + error, + }) + + expect(CashWalletMigration.findOneAndUpdate).toHaveBeenCalledWith( + { _id: "migration-id", lockedBy: "worker-1", cutoverVersion: 2, runId: "run-2" }, + expect.objectContaining({ + $set: expect.objectContaining({ + status: "requires_operator_review", + lastError: "execution failed", + lockedAt: null, + lockedBy: null, + }), + $inc: { attempts: 1 }, + }), + { new: true }, + ) + expect(result).toMatchObject({ + status: "requires_operator_review", + attempts: 2, + lastError: "execution failed", + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index ec1990392..8212e936f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,39 +14,39 @@ resolved "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz" integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== -"@apollo/client@~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0 || ~3.8.0 || ~3.9.0 || ~3.10.0 || ~3.11.0 || ~3.12.0 || ~3.13.0": - version "3.13.9" - resolved "https://registry.npmjs.org/@apollo/client/-/client-3.13.9.tgz" - integrity sha512-RStSzQfL1XwL6/NWd7W8avhGQYTgPCtJ+qHkkTTSj9Upp3VVm6Oppv81YWdXG1FgEpDPW4hvCrTUELdcC4inCQ== +"@apollo/client@3.8.3": + version "3.8.3" + resolved "https://registry.npmjs.org/@apollo/client/-/client-3.8.3.tgz" + integrity sha512-mK86JM6hCpMEBGDgdO9U8ZYS8r9lPjXE1tVGpJMdSFUsIcXpmEfHUAbbFpPtYmxn8Qa7XsYy0dwDaDhpf4UUPw== dependencies: "@graphql-typed-document-node/core" "^3.1.1" - "@wry/caches" "^1.0.0" + "@wry/context" "^0.7.3" "@wry/equality" "^0.5.6" - "@wry/trie" "^0.5.0" + "@wry/trie" "^0.4.3" graphql-tag "^2.12.6" hoist-non-react-statics "^3.3.2" - optimism "^0.18.0" + optimism "^0.17.5" prop-types "^15.7.2" - rehackt "^0.1.0" + response-iterator "^0.2.6" symbol-observable "^4.0.0" ts-invariant "^0.10.3" tslib "^2.3.0" zen-observable-ts "^1.2.5" -"@apollo/client@3.8.3": - version "3.8.3" - resolved "https://registry.npmjs.org/@apollo/client/-/client-3.8.3.tgz" - integrity sha512-mK86JM6hCpMEBGDgdO9U8ZYS8r9lPjXE1tVGpJMdSFUsIcXpmEfHUAbbFpPtYmxn8Qa7XsYy0dwDaDhpf4UUPw== +"@apollo/client@~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0 || ~3.8.0 || ~3.9.0 || ~3.10.0 || ~3.11.0 || ~3.12.0 || ~3.13.0": + version "3.13.9" + resolved "https://registry.npmjs.org/@apollo/client/-/client-3.13.9.tgz" + integrity sha512-RStSzQfL1XwL6/NWd7W8avhGQYTgPCtJ+qHkkTTSj9Upp3VVm6Oppv81YWdXG1FgEpDPW4hvCrTUELdcC4inCQ== dependencies: "@graphql-typed-document-node/core" "^3.1.1" - "@wry/context" "^0.7.3" + "@wry/caches" "^1.0.0" "@wry/equality" "^0.5.6" - "@wry/trie" "^0.4.3" + "@wry/trie" "^0.5.0" graphql-tag "^2.12.6" hoist-non-react-statics "^3.3.2" - optimism "^0.17.5" + optimism "^0.18.0" prop-types "^15.7.2" - response-iterator "^0.2.6" + rehackt "^0.1.0" symbol-observable "^4.0.0" ts-invariant "^0.10.3" tslib "^2.3.0" @@ -213,7 +213,7 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0": +"@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz" integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== @@ -229,7 +229,7 @@ dependencies: tslib "^2.6.2" -"@aws-crypto/util@^5.2.0", "@aws-crypto/util@5.2.0": +"@aws-crypto/util@5.2.0", "@aws-crypto/util@^5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz" integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== @@ -706,7 +706,7 @@ "@smithy/types" "^4.11.0" tslib "^2.6.2" -"@aws-sdk/types@^3.222.0", "@aws-sdk/types@3.968.0": +"@aws-sdk/types@3.968.0", "@aws-sdk/types@^3.222.0": version "3.968.0" resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.968.0.tgz" integrity sha512-Wuumj/1cuiuXTMdHmvH88zbEl+5Pw++fOFQuMCF4yP0R+9k1lwX8rVst+oy99xaxtdluJZXrsccoZoA67ST1Ow== @@ -798,7 +798,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz" integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== -"@babel/core@^7.0.0", "@babel/core@^7.0.0 || ^8.0.0-0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.22.20", "@babel/core@^7.23.9", "@babel/core@^7.27.4", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.8.0": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.22.20", "@babel/core@^7.23.9", "@babel/core@^7.27.4": version "7.28.5" resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz" integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== @@ -1787,7 +1787,7 @@ "@firebase/util" "1.9.3" tslib "^2.1.0" -"@firebase/database-types@^0.10.4", "@firebase/database-types@0.10.4": +"@firebase/database-types@0.10.4", "@firebase/database-types@^0.10.4": version "0.10.4" resolved "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz" integrity sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ== @@ -1857,16 +1857,16 @@ resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz" integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== -"@google-cloud/promisify@^3.0.0": - version "3.0.1" - resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz" - integrity sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA== - "@google-cloud/promisify@<4.1.0": version "4.0.0" resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz" integrity sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g== +"@google-cloud/promisify@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz" + integrity sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA== + "@google-cloud/storage@^6.9.5": version "6.12.0" resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz" @@ -1953,6 +1953,14 @@ tslib "^2.4.0" unixify "1.0.0" +"@graphql-tools/merge@8.3.1": + version "8.3.1" + resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.1.tgz" + integrity sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg== + dependencies: + "@graphql-tools/utils" "8.9.0" + tslib "^2.4.0" + "@graphql-tools/merge@^8.1.2", "@graphql-tools/merge@^8.4.1": version "8.4.2" resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.2.tgz" @@ -1969,14 +1977,6 @@ "@graphql-tools/utils" "^10.9.1" tslib "^2.4.0" -"@graphql-tools/merge@8.3.1": - version "8.3.1" - resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.1.tgz" - integrity sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg== - dependencies: - "@graphql-tools/utils" "8.9.0" - tslib "^2.4.0" - "@graphql-tools/mock@^8.1.2": version "8.7.20" resolved "https://registry.npmjs.org/@graphql-tools/mock/-/mock-8.7.20.tgz" @@ -1994,16 +1994,7 @@ dependencies: tslib "^2.4.0" -"@graphql-tools/schema@^10.0.25": - version "10.0.25" - resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.25.tgz" - integrity sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw== - dependencies: - "@graphql-tools/merge" "^9.1.1" - "@graphql-tools/utils" "^10.9.1" - tslib "^2.4.0" - -"@graphql-tools/schema@^8.0.0", "@graphql-tools/schema@^8.5.1", "@graphql-tools/schema@8.5.1": +"@graphql-tools/schema@8.5.1", "@graphql-tools/schema@^8.0.0", "@graphql-tools/schema@^8.5.1": version "8.5.1" resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.5.1.tgz" integrity sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg== @@ -2013,17 +2004,16 @@ tslib "^2.4.0" value-or-promise "1.0.11" -"@graphql-tools/schema@^9.0.1": - version "9.0.19" - resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz" - integrity sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w== +"@graphql-tools/schema@^10.0.25": + version "10.0.25" + resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.25.tgz" + integrity sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw== dependencies: - "@graphql-tools/merge" "^8.4.1" - "@graphql-tools/utils" "^9.2.1" + "@graphql-tools/merge" "^9.1.1" + "@graphql-tools/utils" "^10.9.1" tslib "^2.4.0" - value-or-promise "^1.0.12" -"@graphql-tools/schema@^9.0.18": +"@graphql-tools/schema@^9.0.1", "@graphql-tools/schema@^9.0.18": version "9.0.19" resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz" integrity sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w== @@ -2033,6 +2023,13 @@ tslib "^2.4.0" value-or-promise "^1.0.12" +"@graphql-tools/utils@8.9.0": + version "8.9.0" + resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.9.0.tgz" + integrity sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg== + dependencies: + tslib "^2.4.0" + "@graphql-tools/utils@^10.9.1": version "10.9.1" resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.9.1.tgz" @@ -2052,13 +2049,6 @@ "@graphql-typed-document-node/core" "^3.1.1" tslib "^2.4.0" -"@graphql-tools/utils@8.9.0": - version "8.9.0" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.9.0.tgz" - integrity sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg== - dependencies: - tslib "^2.4.0" - "@graphql-tools/webpack-loader-runtime@7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/@graphql-tools/webpack-loader-runtime/-/webpack-loader-runtime-7.0.0.tgz" @@ -2078,6 +2068,14 @@ resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@grpc/grpc-js@1.9.4": + version "1.9.4" + resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.4.tgz" + integrity sha512-oEnzYiDuEsBydZBtP84BkpduLsE1nSAO4KrhTLHRzNrIQE647fhchmosTQsJdCo8X9zBBt+l5+fNk+m/yCFJ/Q== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + "@grpc/grpc-js@^1.9.3": version "1.14.0" resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz" @@ -2094,25 +2092,17 @@ "@grpc/proto-loader" "^0.7.0" "@types/node" ">=12.12.47" -"@grpc/grpc-js@1.9.4": - version "1.9.4" - resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.4.tgz" - integrity sha512-oEnzYiDuEsBydZBtP84BkpduLsE1nSAO4KrhTLHRzNrIQE647fhchmosTQsJdCo8X9zBBt+l5+fNk+m/yCFJ/Q== - dependencies: - "@grpc/proto-loader" "^0.7.8" - "@types/node" ">=12.12.47" - -"@grpc/proto-loader@^0.7.0", "@grpc/proto-loader@^0.7.9": - version "0.7.15" - resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz" - integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== +"@grpc/proto-loader@0.7.10": + version "0.7.10" + resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz" + integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ== dependencies: lodash.camelcase "^4.3.0" long "^5.0.0" - protobufjs "^7.2.5" + protobufjs "^7.2.4" yargs "^17.7.2" -"@grpc/proto-loader@^0.7.8": +"@grpc/proto-loader@^0.7.0", "@grpc/proto-loader@^0.7.8", "@grpc/proto-loader@^0.7.9": version "0.7.15" resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz" integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== @@ -2132,16 +2122,6 @@ protobufjs "^7.5.3" yargs "^17.7.2" -"@grpc/proto-loader@0.7.10": - version "0.7.10" - resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz" - integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ== - dependencies: - lodash.camelcase "^4.3.0" - long "^5.0.0" - protobufjs "^7.2.4" - yargs "^17.7.2" - "@hono/node-server@^1.19.9": version "1.19.9" resolved "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz" @@ -2354,13 +2334,6 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - "@jest/schemas@30.0.5": version "30.0.5" resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz" @@ -2368,6 +2341,13 @@ dependencies: "@sinclair/typebox" "^0.34.0" +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jest/source-map@^29.6.3": version "29.6.3" resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz" @@ -2439,19 +2419,7 @@ slash "^3.0.0" write-file-atomic "^5.0.1" -"@jest/types@^29.6.3": - version "29.6.3" - resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" - integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== - dependencies: - "@jest/schemas" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jest/types@^30.0.0", "@jest/types@30.2.0": +"@jest/types@30.2.0", "@jest/types@^30.0.0": version "30.2.0" resolved "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz" integrity sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg== @@ -2464,6 +2432,18 @@ "@types/yargs" "^17.0.33" chalk "^4.1.2" +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@josephg/resolvable@^1.0.0": version "1.0.1" resolved "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz" @@ -2495,14 +2475,6 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": - version "0.3.31" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" - integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" @@ -2511,6 +2483,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@js-sdsl/ordered-map@^4.4.2": version "4.4.2" resolved "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz" @@ -2620,13 +2600,6 @@ resolved "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz" integrity sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w== -"@noble/curves@~1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz" - integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== - dependencies: - "@noble/hashes" "1.3.1" - "@noble/curves@1.2.0": version "1.2.0" resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz" @@ -2634,20 +2607,12 @@ dependencies: "@noble/hashes" "1.3.2" -"@noble/hashes@^1.2.0": - version "1.8.0" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" - integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== - -"@noble/hashes@~1.3.0": - version "1.3.3" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz" - integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== - -"@noble/hashes@~1.3.1": - version "1.3.3" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz" - integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== +"@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" "@noble/hashes@1.3.1": version "1.3.1" @@ -2659,6 +2624,16 @@ resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@^1.2.0": + version "1.8.0" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + +"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": + version "1.3.3" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz" + integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -2667,7 +2642,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -2685,14 +2660,14 @@ resolved "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz" integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== -"@opentelemetry/api-logs@>=0.39.1", "@opentelemetry/api-logs@0.43.0": +"@opentelemetry/api-logs@0.43.0": version "0.43.0" resolved "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.43.0.tgz" integrity sha512-0CXMOYPXgAdLM2OzVkiUfAL6QQwWVhnMfUXCqLsITY42FZ9TxAhZIHkoc4mfVxvPuXsBnRYGR8UQZX86p87z4A== dependencies: "@opentelemetry/api" "^1.0.0" -"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.6.0", "@opentelemetry/api@>=1.0.0 <1.10.0", "@opentelemetry/api@>=1.0.0 <1.7.0", "@opentelemetry/api@>=1.3.0 <1.10.0", "@opentelemetry/api@>=1.3.0 <1.7.0", "@opentelemetry/api@>=1.4.0 <1.7.0": +"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.6.0": version "1.6.0" resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.6.0.tgz" integrity sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g== @@ -2702,13 +2677,6 @@ resolved "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz" integrity sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA== -"@opentelemetry/core@^1.17.0", "@opentelemetry/core@1.30.1": - version "1.30.1" - resolved "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz" - integrity sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ== - dependencies: - "@opentelemetry/semantic-conventions" "1.28.0" - "@opentelemetry/core@1.17.0": version "1.17.0" resolved "https://registry.npmjs.org/@opentelemetry/core/-/core-1.17.0.tgz" @@ -2716,6 +2684,13 @@ dependencies: "@opentelemetry/semantic-conventions" "1.17.0" +"@opentelemetry/core@1.30.1", "@opentelemetry/core@^1.17.0": + version "1.30.1" + resolved "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz" + integrity sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ== + dependencies: + "@opentelemetry/semantic-conventions" "1.28.0" + "@opentelemetry/exporter-trace-otlp-http@^0.43.0": version "0.43.0" resolved "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.43.0.tgz" @@ -2779,7 +2754,7 @@ "@opentelemetry/instrumentation" "^0.46.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation@^0.43.0", "@opentelemetry/instrumentation@0.43.0": +"@opentelemetry/instrumentation@0.43.0", "@opentelemetry/instrumentation@^0.43.0": version "0.43.0" resolved "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz" integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ== @@ -2861,14 +2836,6 @@ resolved "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz" integrity sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g== -"@opentelemetry/resources@^1.17.0", "@opentelemetry/resources@1.30.1": - version "1.30.1" - resolved "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz" - integrity sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA== - dependencies: - "@opentelemetry/core" "1.30.1" - "@opentelemetry/semantic-conventions" "1.28.0" - "@opentelemetry/resources@1.17.0": version "1.17.0" resolved "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.17.0.tgz" @@ -2877,6 +2844,14 @@ "@opentelemetry/core" "1.17.0" "@opentelemetry/semantic-conventions" "1.17.0" +"@opentelemetry/resources@1.30.1", "@opentelemetry/resources@^1.17.0": + version "1.30.1" + resolved "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz" + integrity sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA== + dependencies: + "@opentelemetry/core" "1.30.1" + "@opentelemetry/semantic-conventions" "1.28.0" + "@opentelemetry/sdk-logs@0.43.0": version "0.43.0" resolved "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.43.0.tgz" @@ -2885,14 +2860,6 @@ "@opentelemetry/core" "1.17.0" "@opentelemetry/resources" "1.17.0" -"@opentelemetry/sdk-metrics@^1.9.1": - version "1.30.1" - resolved "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.30.1.tgz" - integrity sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog== - dependencies: - "@opentelemetry/core" "1.30.1" - "@opentelemetry/resources" "1.30.1" - "@opentelemetry/sdk-metrics@1.17.0": version "1.17.0" resolved "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.17.0.tgz" @@ -2902,14 +2869,13 @@ "@opentelemetry/resources" "1.17.0" lodash.merge "^4.6.2" -"@opentelemetry/sdk-trace-base@^1.17.0", "@opentelemetry/sdk-trace-base@1.30.1": +"@opentelemetry/sdk-metrics@^1.9.1": version "1.30.1" - resolved "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz" - integrity sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg== + resolved "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.30.1.tgz" + integrity sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog== dependencies: "@opentelemetry/core" "1.30.1" "@opentelemetry/resources" "1.30.1" - "@opentelemetry/semantic-conventions" "1.28.0" "@opentelemetry/sdk-trace-base@1.17.0": version "1.17.0" @@ -2920,6 +2886,15 @@ "@opentelemetry/resources" "1.17.0" "@opentelemetry/semantic-conventions" "1.17.0" +"@opentelemetry/sdk-trace-base@1.30.1", "@opentelemetry/sdk-trace-base@^1.17.0": + version "1.30.1" + resolved "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz" + integrity sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg== + dependencies: + "@opentelemetry/core" "1.30.1" + "@opentelemetry/resources" "1.30.1" + "@opentelemetry/semantic-conventions" "1.28.0" + "@opentelemetry/sdk-trace-node@^1.17.0": version "1.30.1" resolved "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz" @@ -2932,11 +2907,6 @@ "@opentelemetry/sdk-trace-base" "1.30.1" semver "^7.5.2" -"@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.17.0": - version "1.37.0" - resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz" - integrity sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA== - "@opentelemetry/semantic-conventions@1.17.0": version "1.17.0" resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.0.tgz" @@ -2947,8 +2917,15 @@ resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz" integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== +"@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.17.0": + version "1.37.0" + resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz" + integrity sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA== + "@ory/client@^1.2.6": - version "v1.22.7" + version "1.22.37" + resolved "https://registry.yarnpkg.com/@ory/client/-/client-1.22.37.tgz#9b63d5c04730ccad1536429a5085286f55eb50c2" + integrity sha512-UEYHSveRRD3TCT0JB54ZKQoRMEHn7HTsNsMOjz2t3bzCs5aNjgpjYIBK4z5zE2PwNrcInZyQXfRiwK+LIUaOug== dependencies: axios "^1.6.1" @@ -2990,11 +2967,71 @@ "@otplib/plugin-crypto" "^12.0.1" "@otplib/plugin-thirty-two" "^12.0.1" +"@parcel/watcher-android-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" + integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== + "@parcel/watcher-darwin-arm64@2.5.1": version "2.5.1" resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz" integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== +"@parcel/watcher-darwin-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" + integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== + +"@parcel/watcher-freebsd-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" + integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== + +"@parcel/watcher-linux-arm-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" + integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== + +"@parcel/watcher-linux-arm-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" + integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== + +"@parcel/watcher-linux-arm64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" + integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== + +"@parcel/watcher-linux-arm64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" + integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== + +"@parcel/watcher-linux-x64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" + integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== + +"@parcel/watcher-linux-x64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" + integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== + +"@parcel/watcher-win32-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" + integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== + +"@parcel/watcher-win32-ia32@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" + integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== + +"@parcel/watcher-win32-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" + integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== + "@parcel/watcher@^2.4.1": version "2.5.1" resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz" @@ -3166,6 +3203,11 @@ resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== +"@scure/base@1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz" + integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== + "@scure/base@^1.1.1": version "1.2.6" resolved "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz" @@ -3176,11 +3218,6 @@ resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz" integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== -"@scure/base@1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz" - integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== - "@scure/bip32@1.3.1": version "1.3.1" resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz" @@ -3755,7 +3792,52 @@ resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.85.tgz" integrity sha512-jTikp+i4nO4Ofe6qGm4I3sFeebD1OvueBCHITux5tQKD6umN1c2z4CRGv6K49NIz/qEpUcdr6Qny6K+3yibVFQ== -"@swc/core@*", "@swc/core@^1.3.68", "@swc/core@>=1.2.50", "@swc/core@1.3.85": +"@swc/core-darwin-x64@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.85.tgz#ce623656ee427876423f07d898fbd7fa767d39bd" + integrity sha512-3uHYkjVU+2F+YbVYtq5rH0uCJIztFTALaS3mQEfQUZKXZ5/8jD5titTCRqFKtSlQg0CzaFZgsYsuqwYBmgN0mA== + +"@swc/core-linux-arm-gnueabihf@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.85.tgz#9eb958e3b2c4af0d208dc2d1e71270797605c71b" + integrity sha512-ouHzAHsFaEOkRuoTAOI/8n2m8BQAAnb4vr/xbMhhDOmix0lp5eNsW5Iac/EcJ2uG6B3n7P2K8oycj9SWkj+pfw== + +"@swc/core-linux-arm64-gnu@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.85.tgz#2932636d5414709c56b9a670ca804dc317201a41" + integrity sha512-/Z1CZOWiO+NqJEh1J20PIxQFHMH43upQJ1l7FJ5Z7+MyuYF8WkeJ7OSovau729pBR+38vvvccEJrMZIztfv7hQ== + +"@swc/core-linux-arm64-musl@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.85.tgz#3b312c57ac0b6861d87a7c0f7ff24e232eb792d5" + integrity sha512-gfh7CfKavi076dbMBTzfdawSGcYfZ4+1Q+8aRkSesqepKHcIWIJti8Cf3zB4a6CHNhJe+VN0Gb7DEfumydAm1w== + +"@swc/core-linux-x64-gnu@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.85.tgz#d9fd7ff6ba4e693274604926b82027cb4c2a3306" + integrity sha512-lWVqjHKzofb9q1qrBM4dLqO7CIisp08/xMS5Hz9DWex1gTc5F2b6yJO6Ceqwa256GMweJcdP6A5EvEFQAiZ5dg== + +"@swc/core-linux-x64-musl@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.85.tgz#0a80efac999fb125131385a8937227819549939f" + integrity sha512-EPJmlfqC05TUetnlErxNRyIp7Nc3B2w9abET6oQ/EgldeAeQnZ3M6svMViET/c2QSomgrU3rdP+Qcozkt62/4A== + +"@swc/core-win32-arm64-msvc@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.85.tgz#9808ee0d35eeb315efa7b73edcca80001451c9f3" + integrity sha512-ibckJDZw8kNosciMexwk0z75ZyUhwtiFMV9rSBpup0opa7NNCUCoERCJ1e9LRyMdhsVUoLpZg/KZiHCdTw96hQ== + +"@swc/core-win32-ia32-msvc@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.85.tgz#9b85ba050f333199e88e54ba0dd7b72302af64be" + integrity sha512-hY4MpHGUVQHL1T2kgRXOigDho4DTIpVPYzJ4uyy8VQRbS7GzN5XtvdGP/fA4zp8+2BQjcig+6J7Y92SY15ouNQ== + +"@swc/core-win32-x64-msvc@1.3.85": + version "1.3.85" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.85.tgz#5c7f3f366804b512cf26e1731d8714f0409f1204" + integrity sha512-ktxWOMFJ0iqKn6WUHtXqi4CS7xkyHmrRtjllGRuGqxmLmDX/HSOfuQ55Tm1KXKk5oHLacJkUbOSF2kBrpZ8dpg== + +"@swc/core@1.3.85": version "1.3.85" resolved "https://registry.npmjs.org/@swc/core/-/core-1.3.85.tgz" integrity sha512-qnoxp+2O0GtvRdYnXgR1v8J7iymGGYpx6f6yCK9KxipOZOjrlKILFANYlghQxZyPUfXwK++TFxfSlX4r9wK+kg== @@ -3778,7 +3860,7 @@ resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/jest@^0.2.26", "@swc/jest@^0.2.29": +"@swc/jest@^0.2.29": version "0.2.39" resolved "https://registry.npmjs.org/@swc/jest/-/jest-0.2.39.tgz" integrity sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA== @@ -3955,7 +4037,7 @@ dependencies: dotenv "*" -"@types/eslint@^8.44.2", "@types/eslint@>=8.0.0": +"@types/eslint@^8.44.2": version "8.56.12" resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz" integrity sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g== @@ -3968,17 +4050,16 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== -"@types/express-serve-static-core@^4.17.18": - version "4.19.7" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz" - integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg== +"@types/express-serve-static-core@4.17.31": + version "4.17.31" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz" + integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" - "@types/send" "*" -"@types/express-serve-static-core@^4.17.33": +"@types/express-serve-static-core@^4.17.18", "@types/express-serve-static-core@^4.17.33": version "4.19.7" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz" integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg== @@ -3988,25 +4069,6 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express-serve-static-core@4.17.31": - version "4.17.31" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz" - integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@*", "@types/express@^4.17.15", "@types/express@^4.17.20": - version "4.17.24" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.24.tgz" - integrity sha512-Mbrt4SRlXSTWryOnHAh2d4UQ/E7n9lZyGSi6KgX+4hkuL9soYbLOVXVhnk/ODp12YsGc95f4pOvqywJ6kngUwg== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - "@types/express@4.17.14": version "4.17.14" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz" @@ -4027,6 +4089,16 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/express@^4.17.15", "@types/express@^4.17.20": + version "4.17.24" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.24.tgz" + integrity sha512-Mbrt4SRlXSTWryOnHAh2d4UQ/E7n9lZyGSi6KgX+4hkuL9soYbLOVXVhnk/ODp12YsGc95f4pOvqywJ6kngUwg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/form-data@0.0.33": version "0.0.33" resolved "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz" @@ -4203,7 +4275,7 @@ resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== -"@types/markdown-it@*", "@types/markdown-it@^14.1.1": +"@types/markdown-it@^14.1.1": version "14.1.2" resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz" integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== @@ -4248,20 +4320,10 @@ dependencies: undici-types "~6.21.0" -"@types/node@^10.0.3": - version "10.17.60" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz" - integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== - -"@types/node@^10.1.0": - version "10.17.60" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz" - integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== - -"@types/node@^8.0.0": - version "8.10.66" - resolved "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz" - integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw== +"@types/node@20.7.1": + version "20.7.1" + resolved "https://registry.npmjs.org/@types/node/-/node-20.7.1.tgz" + integrity sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg== "@types/node@>=12.12.47", "@types/node@>=13.7.0": version "24.9.1" @@ -4270,17 +4332,15 @@ dependencies: undici-types "~7.16.0" -"@types/node@>=13.7.0": - version "24.9.1" - resolved "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz" - integrity sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg== - dependencies: - undici-types "~7.16.0" +"@types/node@^10.0.3", "@types/node@^10.1.0": + version "10.17.60" + resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz" + integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@20.7.1": - version "20.7.1" - resolved "https://registry.npmjs.org/@types/node/-/node-20.7.1.tgz" - integrity sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg== +"@types/node@^8.0.0": + version "8.10.66" + resolved "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz" + integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw== "@types/nodemon@^1.19.2": version "1.19.6" @@ -4304,7 +4364,7 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react@*", "@types/react@^18.2.21": +"@types/react@^18.2.21": version "18.3.26" resolved "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz" integrity sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA== @@ -4319,16 +4379,6 @@ dependencies: "@types/node" "*" -"@types/request@^2.48.8": - version "2.48.13" - resolved "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz" - integrity sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg== - dependencies: - "@types/caseless" "*" - "@types/node" "*" - "@types/tough-cookie" "*" - form-data "^2.5.5" - "@types/request@2.48.9": version "2.48.9" resolved "https://registry.npmjs.org/@types/request/-/request-2.48.9.tgz" @@ -4339,6 +4389,16 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/request@^2.48.8": + version "2.48.13" + resolved "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz" + integrity sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.5" + "@types/rimraf@^3.0.2": version "3.0.2" resolved "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz" @@ -4457,7 +4517,7 @@ resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz" integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw== -"@typescript-eslint/eslint-plugin@^5.0.0 || ^6.0.0 || ^7.0.0", "@typescript-eslint/eslint-plugin@^6.7.0": +"@typescript-eslint/eslint-plugin@^6.7.0": version "6.21.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz" integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== @@ -4474,7 +4534,7 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6.0.0 || ^6.0.0-alpha", "@typescript-eslint/parser@^6.7.2": +"@typescript-eslint/parser@^6.7.2": version "6.21.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz" integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== @@ -4526,33 +4586,7 @@ resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz" integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/typescript-estree@^4.33.0": - version "4.33.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz" - integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== - dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@^5.55.0": - version "5.62.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz" - integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== - dependencies: - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/visitor-keys" "5.62.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@5.62.0": +"@typescript-eslint/typescript-estree@5.62.0", "@typescript-eslint/typescript-estree@^5.55.0": version "5.62.0" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz" integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== @@ -4579,19 +4613,18 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@^5.10.0": - version "5.62.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz" - integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== +"@typescript-eslint/typescript-estree@^4.33.0": + version "4.33.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz" + integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.62.0" - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/typescript-estree" "5.62.0" - eslint-scope "^5.1.1" - semver "^7.3.7" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" "@typescript-eslint/utils@6.21.0": version "6.21.0" @@ -4606,6 +4639,20 @@ "@typescript-eslint/typescript-estree" "6.21.0" semver "^7.5.4" +"@typescript-eslint/utils@^5.10.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + "@typescript-eslint/visitor-keys@4.33.0": version "4.33.0" resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz" @@ -4685,16 +4732,16 @@ commander "^8.2.0" globby "^12.0.2" -abbrev@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz" - integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== - abbrev@1: version "1.1.1" resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" @@ -4735,7 +4782,7 @@ acorn-walk@^8.1.1: dependencies: acorn "^8.11.0" -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8, acorn@^8.11.0, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.11.0, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: version "8.15.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -4745,11 +4792,6 @@ adm-zip@0.5.10: resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz" integrity sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ== -agent-base@^7.1.2: - version "7.1.4" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" - integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== - agent-base@6: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" @@ -4757,6 +4799,11 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + ajv-draft-04@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz" @@ -4779,7 +4826,7 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.12.0, ajv@^8.17.1, ajv@^8.5.0, "ajv@4.11.8 - 8": +ajv@^8.0.0, ajv@^8.12.0, ajv@^8.17.1: version "8.17.1" resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== @@ -5154,6 +5201,11 @@ async-retry@^1.2.1, async-retry@^1.3.3: dependencies: retry "0.13.1" +async@3.2.4: + version "3.2.4" + resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + async@^2.6.0: version "2.6.4" resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz" @@ -5166,11 +5218,6 @@ async@^3.2.0, async@^3.2.3, async@~3.2.0: resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== -async@3.2.4: - version "3.2.4" - resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== - asyncjs-util@1.2.12: version "1.2.12" resolved "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.2.12.tgz" @@ -5217,7 +5264,7 @@ axios@^0.26.1: dependencies: follow-redirects "^1.14.8" -axios@^1.12.0, axios@^1.3.4, axios@^1.5.0, axios@^1.6.0, axios@^1.6.1, "axios@>= 0.17.0": +axios@^1.12.0, axios@^1.3.4, axios@^1.5.0, axios@^1.6.0, axios@^1.6.1: version "1.12.2" resolved "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz" integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw== @@ -5368,16 +5415,16 @@ batch@0.6.1: resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== +bech32@2.0.0, bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + bech32@^1.1.2: version "1.1.4" resolved "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== -bech32@^2.0.0, bech32@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz" - integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== - bignumber.js@^9.0.0: version "9.3.1" resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz" @@ -5433,23 +5480,11 @@ bitcoin-cli-ts@^24.0.3: json-rpc-2.0 "^1.6.0" undici "^5.22.1" -bitcoin-ops@^1.3.0, bitcoin-ops@1.4.1: +bitcoin-ops@1.4.1, bitcoin-ops@^1.3.0: version "1.4.1" resolved "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz" integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow== -bitcoinjs-lib@^6.0.0, bitcoinjs-lib@^6.1.4: - version "6.1.7" - resolved "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz" - integrity sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg== - dependencies: - "@noble/hashes" "^1.2.0" - bech32 "^2.0.0" - bip174 "^2.1.1" - bs58check "^3.0.1" - typeforce "^1.11.3" - varuint-bitcoin "^1.1.2" - bitcoinjs-lib@6.1.3: version "6.1.3" resolved "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz" @@ -5474,6 +5509,18 @@ bitcoinjs-lib@6.1.5: typeforce "^1.11.3" varuint-bitcoin "^1.1.2" +bitcoinjs-lib@^6.0.0, bitcoinjs-lib@^6.1.4: + version "6.1.7" + resolved "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz" + integrity sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg== + dependencies: + "@noble/hashes" "^1.2.0" + bech32 "^2.0.0" + bip174 "^2.1.1" + bs58check "^3.0.1" + typeforce "^1.11.3" + varuint-bitcoin "^1.1.2" + bl@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" @@ -5488,53 +5535,15 @@ bluebird@^3.7.2: resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^4.11.8: - version "4.12.2" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz" - integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== - -bn.js@^4.11.9: - version "4.12.2" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz" - integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== - bn.js@5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== -body-parser@^1.19.0, body-parser@^1.20.1, body-parser@1.20.3: - version "1.20.3" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.13.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -body-parser@^2.2.1: - version "2.2.2" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz" - integrity sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA== - dependencies: - bytes "^3.1.2" - content-type "^1.0.5" - debug "^4.4.3" - http-errors "^2.0.0" - iconv-lite "^0.7.0" - on-finished "^2.4.1" - qs "^6.14.1" - raw-body "^3.0.1" - type-is "^2.0.1" +bn.js@^4.11.8, bn.js@^4.11.9: + version "4.12.2" + resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz" + integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== body-parser@1.20.1: version "1.20.1" @@ -5572,6 +5581,39 @@ body-parser@1.20.2: type-is "~1.6.18" unpipe "1.0.0" +body-parser@1.20.3, body-parser@^1.19.0, body-parser@^1.20.1: + version "1.20.3" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^2.2.1: + version "2.2.2" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz" + integrity sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.3" + http-errors "^2.0.0" + iconv-lite "^0.7.0" + on-finished "^2.4.1" + qs "^6.14.1" + raw-body "^3.0.1" + type-is "^2.0.1" + body@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/body/-/body-5.1.0.tgz" @@ -5645,7 +5687,7 @@ brorand@^1.1.0: resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== -browserslist@^4.24.0, browserslist@^4.26.3, "browserslist@>= 4.21.0": +browserslist@^4.24.0, browserslist@^4.26.3: version "4.27.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz" integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw== @@ -5670,14 +5712,6 @@ bs58@^5.0.0: dependencies: base-x "^4.0.0" -bs58check@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz" - integrity sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ== - dependencies: - "@noble/hashes" "^1.2.0" - bs58 "^5.0.0" - bs58check@<3.0.0: version "2.1.2" resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz" @@ -5687,6 +5721,14 @@ bs58check@<3.0.0: create-hash "^1.1.0" safe-buffer "^5.1.2" +bs58check@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz" + integrity sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ== + dependencies: + "@noble/hashes" "^1.2.0" + bs58 "^5.0.0" + bser@2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" @@ -5694,9 +5736,9 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -bson@^5.5.0: +bson@^5.4.0, bson@^5.5.0: version "5.5.1" - resolved "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz" + resolved "https://registry.yarnpkg.com/bson/-/bson-5.5.1.tgz#f5849d405711a7f23acdda9a442375df858e6833" integrity sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g== bson@^6.10.4: @@ -5730,16 +5772,16 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -bytes@^3.1.2, bytes@~3.1.2, bytes@3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - bytes@1: version "1.0.0" resolved "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz" integrity sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ== +bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" @@ -5951,16 +5993,16 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" - integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== - clone@2.x: version "2.1.2" resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + cluster-key-slot@^1.1.0: version "1.1.2" resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz" @@ -6003,16 +6045,16 @@ color-support@^1.1.2: resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -colorette@^2.0.7: - version "2.0.20" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - colorette@2.0.19: version "2.0.19" resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== +colorette@^2.0.7: + version "2.0.20" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + colors@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz" @@ -6045,12 +6087,7 @@ commander@^8.2.0: resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commander@^9.1.0: - version "9.5.0" - resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" - integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== - -commander@^9.5.0: +commander@^9.1.0, commander@^9.5.0: version "9.5.0" resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== @@ -6136,11 +6173,6 @@ console.table@^0.10.0: dependencies: easy-table "1.1.0" -content-disposition@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz" - integrity sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q== - content-disposition@0.5.4: version "0.5.4" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" @@ -6148,6 +6180,11 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" +content-disposition@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz" + integrity sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q== + content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" @@ -6171,32 +6208,27 @@ cookie-parser@^1.4.6: cookie "0.7.2" cookie-signature "1.0.6" -cookie-signature@^1.2.1: - version "1.2.2" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz" - integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@^0.5.0, cookie@0.5.0: +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + +cookie@0.5.0, cookie@^0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookie@^0.7.1: - version "0.7.2" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz" - integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== - cookie@0.7.1: version "0.7.1" resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== -cookie@0.7.2: +cookie@0.7.2, cookie@^0.7.1: version "0.7.2" resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== @@ -6213,7 +6245,7 @@ core-util-is@~1.0.0: resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cors@^2.8.5, cors@2.8.5: +cors@2.8.5, cors@^2.8.5: version "2.8.5" resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -6297,7 +6329,7 @@ csv-writer@^1.6.0: resolved "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz" integrity sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g== -d@^1.0.1, d@^1.0.2, d@1: +d@1, d@^1.0.1, d@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/d/-/d-1.0.2.tgz" integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== @@ -6332,16 +6364,16 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -dataloader@^2.2.2: - version "2.2.3" - resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz" - integrity sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA== - dataloader@2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.1.0.tgz" integrity sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ== +dataloader@^2.2.2: + version "2.2.3" + resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz" + integrity sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA== + datauri@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/datauri/-/datauri-4.1.0.tgz" @@ -6372,34 +6404,20 @@ dayjs@^1.11.9: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz" integrity sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA== -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: - ms "^2.1.1" + ms "2.0.0" -debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3, debug@4, debug@4.x: +debug@4, debug@4.x, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -6407,6 +6425,13 @@ debug@4.3.4: dependencies: ms "2.1.2" +debug@^3.1.0, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + dedent@^1.0.0, dedent@^1.5.1: version "1.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz" @@ -6472,7 +6497,7 @@ denque@^2.1.0: resolved "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== -depd@^2.0.0, depd@~2.0.0, depd@2.0.0: +depd@2.0.0, depd@^2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -6792,7 +6817,7 @@ easy-table@1.1.0: optionalDependencies: wcwidth ">=1.0.1" -ecdsa-sig-formatter@^1.0.11, ecdsa-sig-formatter@1.0.11: +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== @@ -6856,7 +6881,7 @@ emoji-regex@^9.2.2: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -encodeurl@^2.0.0: +encodeurl@^2.0.0, encodeurl@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== @@ -6866,11 +6891,6 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - encoding-sniffer@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz" @@ -7126,7 +7146,7 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^9.0.0, "eslint-config-prettier@>= 7.0.0 <10.0.0 || >=10.1.0": +eslint-config-prettier@^9.0.0: version "9.1.2" resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz" integrity sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ== @@ -7213,7 +7233,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -"eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0", eslint@^8.49.0, eslint@>=7.0.0, eslint@>=8.0.0: +eslint@^8.49.0: version "8.57.1" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -7281,16 +7301,16 @@ espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0, esprima@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - esprima@1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz" integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + esquery@^1.4.2: version "1.6.0" resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" @@ -7305,12 +7325,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^4.2.0: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -7348,16 +7363,16 @@ eventemitter2@~0.4.13: resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz" integrity sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ== -events@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - events@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz" integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + eventsource-parser@^3.0.0, eventsource-parser@^3.0.1: version "3.0.6" resolved "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz" @@ -7427,31 +7442,68 @@ express-unless@^2.1.3: resolved "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz" integrity sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ== -express@^4.17.1, express@^4.18.2, "express@>= 4.11": - version "4.21.2" - resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" - integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== +express@4.18.2: + version "4.18.2" + resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.3" + body-parser "1.20.1" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.7.1" + cookie "0.5.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~2.0.0" + encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.3.1" + finalhandler "1.2.0" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.3" + merge-descriptors "1.0.1" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.12" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +express@^4.17.1, express@^4.18.2: + version "4.21.2" + resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" qs "6.13.0" range-parser "~1.2.1" @@ -7498,43 +7550,6 @@ express@^5.0.1: type-is "^2.0.1" vary "^1.1.2" -express@4.18.2: - version "4.18.2" - resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.1" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - ext@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz" @@ -7613,13 +7628,6 @@ fast-uri@^3.0.1: resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz" integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== -fast-xml-parser@^4.2.2, fast-xml-parser@^4.4.1: - version "4.5.3" - resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz" - integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig== - dependencies: - strnum "^1.1.1" - fast-xml-parser@5.2.5: version "5.2.5" resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz" @@ -7627,6 +7635,13 @@ fast-xml-parser@5.2.5: dependencies: strnum "^2.1.0" +fast-xml-parser@^4.2.2, fast-xml-parser@^4.4.1: + version "4.5.3" + resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz" + integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig== + dependencies: + strnum "^1.1.1" + fastq@^1.6.0: version "1.19.1" resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" @@ -7634,13 +7649,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -faye-websocket@~0.10.0: - version "0.10.0" - resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz" - integrity sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ== - dependencies: - websocket-driver ">=0.5.1" - faye-websocket@0.11.4: version "0.11.4" resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" @@ -7648,6 +7656,13 @@ faye-websocket@0.11.4: dependencies: websocket-driver ">=0.5.1" +faye-websocket@~0.10.0: + version "0.10.0" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz" + integrity sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ== + dependencies: + websocket-driver ">=0.5.1" + fb-watchman@^2.0.0, fb-watchman@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" @@ -7711,18 +7726,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@^2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz" - integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== - dependencies: - debug "^4.4.0" - encodeurl "^2.0.0" - escape-html "^1.0.3" - on-finished "^2.4.1" - parseurl "^1.3.3" - statuses "^2.0.1" - finalhandler@1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" @@ -7762,6 +7765,18 @@ finalhandler@1.3.1: statuses "2.0.1" unpipe "~1.0.0" +finalhandler@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz" + integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + find-cache-dir@^3.3.1: version "3.3.2" resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" @@ -7898,31 +7913,7 @@ form-data-encoder@^1.7.2: resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.9.0.tgz" integrity sha512-rahaRMkN8P8d/tgK/BLPX+WBVM27NbvdXBxqQujBtkDAIFspaRqN7Od7lfdGQA6KAD+f82fYCLBq1ipvcu8qLw== -form-data@^2.2.0: - version "2.5.5" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz" - integrity sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.35" - safe-buffer "^5.2.1" - -form-data@^2.5.0: - version "2.5.5" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz" - integrity sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.35" - safe-buffer "^5.2.1" - -form-data@^2.5.5: +form-data@^2.2.0, form-data@^2.5.0, form-data@^2.5.5: version "2.5.5" resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz" integrity sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A== @@ -7958,16 +7949,16 @@ forwarded@0.2.0: resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz" - integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== - fresh@0.5.2: version "0.5.2" resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + fs-extra@^10.0.1: version "10.1.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" @@ -8064,7 +8055,7 @@ gaze@^1.1.0: dependencies: globule "^1.0.0" -gcp-metadata@^5.2.0, gcp-metadata@^5.3.0: +gcp-metadata@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz" integrity sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w== @@ -8247,19 +8238,7 @@ glob@~5.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@~7.1.1: - version "7.1.7" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@~7.1.6: +glob@~7.1.1, glob@~7.1.6: version "7.1.7" resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== @@ -8306,7 +8285,7 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -globby@^11.0.3, globby@^11.1.0, globby@11.1.0: +globby@11.1.0, globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -8406,16 +8385,16 @@ google-p12-pem@^4.0.0: dependencies: node-forge "^1.3.1" -google-protobuf@^3.21.2: - version "3.21.4" - resolved "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz" - integrity sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ== - google-protobuf@3.15.8: version "3.15.8" resolved "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.15.8.tgz" integrity sha512-2jtfdqTaSxk0cuBJBtTTWsot4WtR9RVr2rXg7x7OoqiuOKopPrwXpM1G4dXIkLcUNRh3RKzz76C8IOkksZSeOw== +google-protobuf@^3.21.2: + version "3.21.4" + resolved "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz" + integrity sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ== + gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" @@ -8431,7 +8410,7 @@ graphemer@^1.4.0: resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -"graphql-middleware@^2.0.0 || ^3.0.0 || ^4.0.0 || ^6.0.0", graphql-middleware@^6.1.33: +graphql-middleware@^6.1.33: version "6.1.35" resolved "https://registry.npmjs.org/graphql-middleware/-/graphql-middleware-6.1.35.tgz" integrity sha512-azawK7ApUYtcuPGRGBR9vDZu795pRuaFhO5fgomdJppdfKRt7jwncuh0b7+D3i574/4B+16CNWgVpnGVlg3ZCg== @@ -8475,7 +8454,7 @@ graphql-shield@^7.6.4: tslib "^2.4.0" yup "^0.32.0" -"graphql-subscriptions@^1.0.0 || ^2.0.0 || ^3.0.0", graphql-subscriptions@^2.0.0: +graphql-subscriptions@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-2.0.0.tgz" integrity sha512-s6k2b8mmt9gF9pEfkxsaO1lTxaySfKoEJzEfmwguBbQ//Oq23hIXCfR1hm4kdh5hnR20RdwB+s3BCb+0duHSZA== @@ -8499,16 +8478,23 @@ graphql-tools@^9.0.0: optionalDependencies: "@apollo/client" "~3.2.5 || ~3.3.0 || ~3.4.0 || ~3.5.0 || ~3.6.0 || ~3.7.0 || ~3.8.0 || ~3.9.0 || ~3.10.0 || ~3.11.0 || ~3.12.0 || ~3.13.0" -graphql-ws@^5.13.1, graphql-ws@^5.5.5, "graphql-ws@^5.5.5 || ^6.0.3": +graphql-ws@^5.13.1: version "5.16.2" resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.2.tgz" integrity sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ== -"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "graphql@^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "graphql@^14.2.1 || ^15.0.0 || ^16.0.0", "graphql@^14.6.0 || ^15.0.0 || ^16.0.0", "graphql@^15.0.0 || ^16.0.0", "graphql@^15.3.0 || ^16.0.0", "graphql@^15.7.2 || ^16.0.0", graphql@^16.2.0, graphql@^16.3.0, graphql@^16.8.0, "graphql@>=0.11 <=16", "graphql@14.x || 15.x || 16.x": +graphql@^16.3.0, graphql@^16.8.0: version "16.11.0" resolved "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz" integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw== +grpc-tools@^1.12.4: + version "1.13.0" + resolved "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.13.0.tgz" + integrity sha512-7CbkJ1yWPfX0nHjbYG58BQThNhbICXBZynzCUxCb3LzX5X9B3hQbRY2STiRgIEiLILlK9fgl0z0QVGwPCdXf5g== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.5" + grpc_tools_node_protoc_ts@^5.3.3: version "5.3.3" resolved "https://registry.npmjs.org/grpc_tools_node_protoc_ts/-/grpc_tools_node_protoc_ts-5.3.3.tgz" @@ -8517,13 +8503,6 @@ grpc_tools_node_protoc_ts@^5.3.3: google-protobuf "3.15.8" handlebars "4.7.7" -grpc-tools@^1.12.4: - version "1.13.0" - resolved "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.13.0.tgz" - integrity sha512-7CbkJ1yWPfX0nHjbYG58BQThNhbICXBZynzCUxCb3LzX5X9B3hQbRY2STiRgIEiLILlK9fgl0z0QVGwPCdXf5g== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.5" - grunt-cli@~1.4.3: version "1.4.3" resolved "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz" @@ -8644,7 +8623,7 @@ grunt-sass@^3.0.2: resolved "https://registry.npmjs.org/grunt-sass/-/grunt-sass-3.1.0.tgz" integrity sha512-90s27H7FoCDcA8C8+R0GwC+ntYD3lG6S/jqcavWm3bn9RiJTmSfOvfbFa1PXx4NbBWuiGQMLfQTj/JvvqT5w6A== -grunt@>=0.4.5, grunt@>=1, grunt@>=1.4.1, grunt@~1.5.3: +grunt@~1.5.3: version "1.5.3" resolved "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz" integrity sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ== @@ -8699,25 +8678,25 @@ gzip-size@^5.1.1: duplexer "^0.1.1" pify "^4.0.1" -handlebars@^4.7.7: - version "4.7.8" - resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz" - integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== +handlebars@4.7.7: + version "4.7.7" + resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== dependencies: minimist "^1.2.5" - neo-async "^2.6.2" + neo-async "^2.6.0" source-map "^0.6.1" wordwrap "^1.0.0" optionalDependencies: uglify-js "^3.1.4" -handlebars@4.7.7: - version "4.7.7" - resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz" - integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== +handlebars@^4.7.7: + version "4.7.8" + resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== dependencies: minimist "^1.2.5" - neo-async "^2.6.0" + neo-async "^2.6.2" source-map "^0.6.1" wordwrap "^1.0.0" optionalDependencies: @@ -8834,11 +8813,6 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hono@^4: - version "4.11.5" - resolved "https://registry.npmjs.org/hono/-/hono-4.11.5.tgz" - integrity sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g== - hooker@~0.2.3: version "0.2.3" resolved "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" @@ -8884,6 +8858,22 @@ http-basic@^8.1.1: http-response-object "^3.0.1" parse-cache-control "^1.0.1" +http-cache-semantics@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz" @@ -8905,17 +8895,6 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - http-parser-js@>=0.5.1: version "0.5.10" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz" @@ -8988,7 +8967,14 @@ ibex-client@^2.2.0: api "^6.1.2" node-cache "^5.1.2" -iconv-lite@^0.6.3: +iconv-lite@0.4.24, iconv-lite@~0.4.13: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@0.6.3, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -9002,20 +8988,6 @@ iconv-lite@^0.7.0, iconv-lite@~0.7.0: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -iconv-lite@~0.4.13, iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" @@ -9051,7 +9023,7 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@1.4.2: +import-in-the-middle@1.4.2, import-in-the-middle@1.7.1: version "1.4.2" resolved "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz" integrity sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw== @@ -9061,16 +9033,6 @@ import-in-the-middle@1.4.2: cjs-module-lexer "^1.2.2" module-details-from-path "^1.0.3" -import-in-the-middle@1.7.1: - version "1.7.1" - resolved "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.7.1.tgz" - integrity sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg== - dependencies: - acorn "^8.8.2" - acorn-import-assertions "^1.9.0" - cjs-module-lexer "^1.2.2" - module-details-from-path "^1.0.3" - import-local@^3.0.2: version "3.2.0" resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" @@ -9097,7 +9059,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4, inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -9136,7 +9098,7 @@ interpret@~1.1.0: resolved "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz" integrity sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA== -invoices@^3.0.0, invoices@3.0.0: +invoices@3.0.0, invoices@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/invoices/-/invoices-3.0.0.tgz" integrity sha512-/WDTkfU2RMelQpQ54BwZssqGXYNWbPnWkZ/9QV57vAvD3RLdCDbhDuucOGti8CK3sgk8nmhRV6V0WfMrxojMmA== @@ -9155,7 +9117,7 @@ ioredis-cache@^2.0.0: dependencies: ioredis "4 - 5" -ioredis@^5.3.2, "ioredis@4 - 5": +"ioredis@4 - 5", ioredis@^5.3.2: version "5.8.2" resolved "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz" integrity sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q== @@ -9175,16 +9137,16 @@ ip-address@^10.0.1: resolved "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz" integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA== -ipaddr.js@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" - integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +ipaddr.js@^2.1.0: + version "2.2.0" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz" @@ -9603,11 +9565,6 @@ jackspeak@^4.1.1: dependencies: "@isaacs/cliui" "^8.0.2" -jest_workaround@^0.79.19: - version "0.79.19" - resolved "https://registry.npmjs.org/jest_workaround/-/jest_workaround-0.79.19.tgz" - integrity sha512-g/MtKSwyb4Ohnd5GHeJaduTgznkyst81x+eUBGOSGK7f8doWuRMPpt6XM/13sM2jLB2QNzT/7Djj7o2PhsozIA== - jest-changed-files@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" @@ -9733,6 +9690,24 @@ jest-get-type@^29.6.3: resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz" integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== +jest-haste-map@30.2.0: + version "30.2.0" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz" + integrity sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw== + dependencies: + "@jest/types" "30.2.0" + "@types/node" "*" + anymatch "^3.1.3" + fb-watchman "^2.0.2" + graceful-fs "^4.2.11" + jest-regex-util "30.0.1" + jest-util "30.2.0" + jest-worker "30.2.0" + micromatch "^4.0.8" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.3" + jest-haste-map@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz" @@ -9752,24 +9727,6 @@ jest-haste-map@^29.7.0: optionalDependencies: fsevents "^2.3.2" -jest-haste-map@30.2.0: - version "30.2.0" - resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz" - integrity sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw== - dependencies: - "@jest/types" "30.2.0" - "@types/node" "*" - anymatch "^3.1.3" - fb-watchman "^2.0.2" - graceful-fs "^4.2.11" - jest-regex-util "30.0.1" - jest-util "30.2.0" - jest-worker "30.2.0" - micromatch "^4.0.8" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.3" - jest-junit@^16.0.0: version "16.0.0" resolved "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz" @@ -9827,16 +9784,16 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-regex-util@^29.6.3: - version "29.6.3" - resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" - integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== - jest-regex-util@30.0.1: version "30.0.1" resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz" integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + jest-resolve-dependencies@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz" @@ -9845,7 +9802,7 @@ jest-resolve-dependencies@^29.7.0: jest-regex-util "^29.6.3" jest-snapshot "^29.7.0" -jest-resolve@*, jest-resolve@^29.7.0: +jest-resolve@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== @@ -9941,18 +9898,6 @@ jest-snapshot@^29.7.0: pretty-format "^29.7.0" semver "^7.5.3" -jest-util@^29.7.0: - version "29.7.0" - resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" - integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - jest-util@30.2.0: version "30.2.0" resolved "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz" @@ -9965,6 +9910,18 @@ jest-util@30.2.0: graceful-fs "^4.2.11" picomatch "^4.0.2" +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz" @@ -9991,16 +9948,6 @@ jest-watcher@^29.7.0: jest-util "^29.7.0" string-length "^4.0.1" -jest-worker@^29.7.0: - version "29.7.0" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" - integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== - dependencies: - "@types/node" "*" - jest-util "^29.7.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" - jest-worker@30.2.0: version "30.2.0" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz" @@ -10012,7 +9959,17 @@ jest-worker@30.2.0: merge-stream "^2.0.0" supports-color "^8.1.1" -jest@*, jest@^29.7.0: +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== @@ -10022,6 +9979,11 @@ jest@*, jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" +jest_workaround@^0.79.19: + version "0.79.19" + resolved "https://registry.npmjs.org/jest_workaround/-/jest_workaround-0.79.19.tgz" + integrity sha512-g/MtKSwyb4Ohnd5GHeJaduTgznkyst81x+eUBGOSGK7f8doWuRMPpt6XM/13sM2jLB2QNzT/7Djj7o2PhsozIA== + jose@^4.15.4: version "4.15.9" resolved "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz" @@ -10062,7 +10024,7 @@ js-sha1@^0.6.0: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: +js-yaml@^3.13.1, js-yaml@~3.14.0: version "3.14.1" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -10077,14 +10039,6 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js-yaml@~3.14.0: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js2xmlparser@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz" @@ -10202,16 +10156,16 @@ json5@^2.2.0, json5@^2.2.2, json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonc-parser@^3.2.0: - version "3.3.1" - resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz" - integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== - jsonc-parser@3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz" integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== +jsonc-parser@^3.2.0: + version "3.3.1" + resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz" + integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== + jsonfile@^6.0.1: version "6.2.0" resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz" @@ -10386,7 +10340,7 @@ liftup@~3.0.1: rechoir "^0.7.0" resolve "^1.19.0" -lightning@^9.13.3, lightning@9.14.0: +lightning@9.14.0, lightning@^9.13.3: version "9.14.0" resolved "https://registry.npmjs.org/lightning/-/lightning-9.14.0.tgz" integrity sha512-08TEh9McKE6uMlCLulNPPyMzZcMBPzZDo2BMnRL+ImnkKZp8QCjioK4jdsl/41npDTsIgeSYdfk8vUrS0zSZng== @@ -10622,6 +10576,18 @@ loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lru-cache@6.0.0, lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +"lru-cache@7.10.1 - 7.13.1": + version "7.13.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz" + integrity sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ== + lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" @@ -10639,18 +10605,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lru-cache@^6.0.0, lru-cache@6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -"lru-cache@7.10.1 - 7.13.1": - version "7.13.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz" - integrity sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ== - lru-memoizer@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz" @@ -10751,7 +10705,7 @@ markdown-it-anchor@^8.6.7: resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz" integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== -markdown-it@*, markdown-it@^14.1.0: +markdown-it@^14.1.0: version "14.1.0" resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz" integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== @@ -10802,16 +10756,16 @@ mdurl@^2.0.0: resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz" integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== -media-typer@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz" - integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + medici@^6.2.0: version "6.3.3" resolved "https://registry.npmjs.org/medici/-/medici-6.3.3.tgz" @@ -10838,11 +10792,6 @@ memory-pager@^1.0.2: resolved "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz" integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== -merge-descriptors@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz" - integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" @@ -10853,6 +10802,11 @@ merge-descriptors@1.0.3: resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" @@ -10895,24 +10849,19 @@ migrate-mongo@^10.0.0: date-fns "^2.28.0" fn-args "^5.0.0" fs-extra "^10.0.1" - lodash "^4.17.21" - p-each-series "^2.2.0" - -mime-db@^1.54.0: - version "1.54.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz" - integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== - -"mime-db@>= 1.43.0 < 2": - version "1.54.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz" - integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + lodash "^4.17.21" + p-each-series "^2.2.0" mime-db@1.52.0: version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" @@ -10927,16 +10876,16 @@ mime-types@^3.0.0, mime-types@^3.0.2: dependencies: mime-db "^1.54.0" -mime@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz" - integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== - mime@1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimer@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/mimer/-/mimer-2.0.2.tgz" @@ -10957,28 +10906,35 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^10.0.3: - version "10.0.3" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz" - integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== - dependencies: - "@isaacs/brace-expansion" "^5.0.0" - -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2, "minimatch@2 || 3": +"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== +minimatch@9.0.1: + version "9.0.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== + dependencies: + brace-expansion "^2.0.1" + +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: brace-expansion "^2.0.1" -minimatch@^5.1.0: +minimatch@^10.0.3: + version "10.0.3" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz" + integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== + dependencies: + "@isaacs/brace-expansion" "^5.0.0" + +minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -10992,34 +10948,13 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimatch@~3.0.2: - version "3.0.8" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz" - integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== - dependencies: - brace-expansion "^1.1.7" - -minimatch@~3.0.4: +minimatch@~3.0.2, minimatch@~3.0.4: version "3.0.8" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz" integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== dependencies: brace-expansion "^1.1.7" -minimatch@9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz" - integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== - dependencies: - brace-expansion "^2.0.1" - -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" @@ -11032,16 +10967,16 @@ minipass@^3.0.0: dependencies: yallist "^4.0.0" -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - minipass@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" @@ -11103,7 +11038,18 @@ mongodb-connection-string-url@^3.0.2: "@types/whatwg-url" "^11.0.2" whatwg-url "^14.1.0 || ^13.0.0" -"mongodb@^4.4.1 || ^5.0.0", mongodb@5.9.2: +mongodb@5.8.1: + version "5.8.1" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.8.1.tgz#dc201adfbd6c6d73401cdcf12ebdb75f14771faf" + integrity sha512-wKyh4kZvm6NrCPH8AxyzXm3JBoEf4Xulo0aUWh3hCgwgYJxyQ1KLST86ZZaSWdj6/kxYUA3+YZuyADCE61CMSg== + dependencies: + bson "^5.4.0" + mongodb-connection-string-url "^2.6.0" + socks "^2.7.1" + optionalDependencies: + "@mongodb-js/saslprep" "^1.1.0" + +mongodb@5.9.2: version "5.9.2" resolved "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz" integrity sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ== @@ -11123,7 +11069,20 @@ mongodb@^6.1.0: bson "^6.10.4" mongodb-connection-string-url "^3.0.2" -mongoose@^7.8.4, "mongoose@5 - 8": +"mongoose@5 - 8", mongoose@~7.5.1: + version "7.5.4" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.5.4.tgz#c83d0a3de8e1a13c19ce6cc5810bf93f0fb4c2a2" + integrity sha512-u97BOfBOoCxysnH5X0WeF/O39DO1di75dYU75xaSs9mL3Si0qmP0qLWvWpBRdVkiiRVw+eaqJyKwaq6RvKPVZw== + dependencies: + bson "^5.4.0" + kareem "2.5.1" + mongodb "5.8.1" + mpath "0.9.0" + mquery "5.0.0" + ms "2.1.3" + sift "16.0.1" + +mongoose@^7.8.4: version "7.8.7" resolved "https://registry.npmjs.org/mongoose/-/mongoose-7.8.7.tgz" integrity sha512-5Bo4CrUxrPITrhMKsqUTOkXXo2CoRC5tXxVQhnddCzqDMwRXfyStrxj1oY865g8gaekSBhxAeNkYyUSJvGm9Hw== @@ -11141,27 +11100,27 @@ moo@^0.5.1: resolved "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz" integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== -morgan@^1.10.0: - version "1.10.1" - resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz" - integrity sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A== +morgan@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== dependencies: basic-auth "~2.0.1" debug "2.6.9" depd "~2.0.0" on-finished "~2.3.0" - on-headers "~1.1.0" + on-headers "~1.0.2" -morgan@1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" - integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== +morgan@^1.10.0: + version "1.10.1" + resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz" + integrity sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A== dependencies: basic-auth "~2.0.1" debug "2.6.9" depd "~2.0.0" on-finished "~2.3.0" - on-headers "~1.0.2" + on-headers "~1.1.0" mpath@0.9.0: version "0.9.0" @@ -11175,11 +11134,6 @@ mquery@5.0.0: dependencies: debug "4.x" -ms@^2.1.1, ms@^2.1.3, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -11190,6 +11144,11 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mustache@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz" @@ -11210,16 +11169,16 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -negotiator@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz" - integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== - negotiator@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + neo-async@^2.6.0, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" @@ -11334,14 +11293,7 @@ node-source-walk@^4.0.0, node-source-walk@^4.2.0, node-source-walk@^4.2.2: dependencies: "@babel/parser" "^7.0.0" -node-source-walk@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/node-source-walk/-/node-source-walk-5.0.2.tgz" - integrity sha512-Y4jr/8SRS5hzEdZ7SGuvZGwfORvNsSsNRwDXx5WisiqzsVfeftDvRgfeqWNgZvWSJbgubTRVRYBzK6UO+ErqjA== - dependencies: - "@babel/parser" "^7.21.4" - -node-source-walk@^5.0.1: +node-source-walk@^5.0.0, node-source-walk@^5.0.1: version "5.0.2" resolved "https://registry.npmjs.org/node-source-walk/-/node-source-walk-5.0.2.tgz" integrity sha512-Y4jr/8SRS5hzEdZ7SGuvZGwfORvNsSsNRwDXx5WisiqzsVfeftDvRgfeqWNgZvWSJbgubTRVRYBzK6UO+ErqjA== @@ -11510,7 +11462,7 @@ oas-validator@^5.0.8: should "^13.2.1" yaml "^1.10.0" -oas@^20.0.0, oas@^20.11.0, oas@^20.5.0: +oas@^20.11.0, oas@^20.5.0: version "20.11.0" resolved "https://registry.npmjs.org/oas/-/oas-20.11.0.tgz" integrity sha512-Eio2qil8z86PD8KJTdyGPN7hbcF3dV0gc5B6mkSQgL3MLMVFE/XyVAmq/9BRh51zkIXOPmZpSIF3nStADbnkHA== @@ -11616,7 +11568,7 @@ on-exit-leak-free@^2.1.0: resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz" integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== -on-finished@^2.4.1, on-finished@2.4.1: +on-finished@2.4.1, on-finished@^2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -11654,7 +11606,7 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -openapi-types@^12.1.0, openapi-types@>=7: +openapi-types@^12.1.0: version "12.1.3" resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== @@ -11929,16 +11881,6 @@ path-scurry@^2.0.0: lru-cache "^11.0.0" minipass "^7.1.2" -path-to-regexp@^6.2.0: - version "6.3.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz" - integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== - -path-to-regexp@^8.0.0: - version "8.3.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz" - integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== - path-to-regexp@0.1.12: version "0.1.12" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" @@ -11949,6 +11891,16 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@^6.2.0: + version "6.3.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + +path-to-regexp@^8.0.0: + version "8.3.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz" + integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -11959,16 +11911,16 @@ pg-cloudflare@^1.2.7: resolved "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz" integrity sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg== -pg-connection-string@^2.9.1: - version "2.9.1" - resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz" - integrity sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w== - pg-connection-string@2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== +pg-connection-string@^2.9.1: + version "2.9.1" + resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz" + integrity sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz" @@ -11995,7 +11947,7 @@ pg-types@2.2.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@^8.11.3, pg@>=8.0: +pg@^8.11.3: version "8.16.3" resolved "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz" integrity sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw== @@ -12078,7 +12030,7 @@ pino-std-serializers@^6.0.0, pino-std-serializers@^6.2.2: resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz" integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== -pino@^8.17.1, pino@8.21.0: +pino@8.21.0, pino@^8.17.1: version "8.21.0" resolved "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz" integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== @@ -12148,7 +12100,7 @@ postcss-values-parser@^6.0.2: is-url-superb "^4.0.0" quote-unquote "^1.0.0" -postcss@^8.1.7, postcss@^8.2.9, postcss@^8.4.19, postcss@^8.4.23: +postcss@^8.1.7, postcss@^8.4.19, postcss@^8.4.23: version "8.5.6" resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== @@ -12233,7 +12185,7 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^3.0.3, prettier@>=3.0.0: +prettier@^3.0.3: version "3.6.2" resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz" integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== @@ -12338,28 +12290,10 @@ protobufjs-cli@1.1.1: tmp "^0.2.1" uglify-js "^3.7.7" -protobufjs@^7.0.0, protobufjs@^7.2.4, protobufjs@^7.2.5, protobufjs@^7.5.3: - version "7.5.4" - resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz" - integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" - -protobufjs@7.2.4: - version "7.2.4" - resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz" - integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== +protobufjs@7.2.4, protobufjs@7.2.5, protobufjs@^7.0.0, protobufjs@^7.2.4, protobufjs@^7.2.5, protobufjs@^7.5.3: + version "7.2.5" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" + integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -12446,13 +12380,6 @@ pushdata-bitcoin@1.0.1: dependencies: bitcoin-ops "^1.3.0" -qs@^6.10.5, qs@^6.12.3, qs@^6.14.0, qs@^6.14.1, qs@^6.4.0, qs@^6.9.4: - version "6.14.1" - resolved "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz" - integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ== - dependencies: - side-channel "^1.1.0" - qs@6.11.0: version "6.11.0" resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" @@ -12467,6 +12394,13 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" +qs@^6.10.5, qs@^6.12.3, qs@^6.14.0, qs@^6.14.1, qs@^6.4.0, qs@^6.9.4: + version "6.14.1" + resolved "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz" + integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ== + dependencies: + side-channel "^1.1.0" + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" @@ -12511,24 +12445,6 @@ rate-limiter-flexible@^3.0.0: resolved "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-3.0.6.tgz" integrity sha512-tlvbee6lyse/XTWmsuBDS4MT8N65FyM151bPmQlFyfhv9+RIHs7d3rSTXoz0j35H910dM01mH0yTIeWYo8+aAw== -raw-body@^3.0.0, raw-body@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz" - integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== - dependencies: - bytes "~3.1.2" - http-errors "~2.0.1" - iconv-lite "~0.7.0" - unpipe "~1.0.0" - -raw-body@~1.1.0: - version "1.1.7" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz" - integrity sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg== - dependencies: - bytes "1" - string_decoder "0.10" - raw-body@2.5.1: version "2.5.1" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" @@ -12549,6 +12465,24 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^3.0.0, raw-body@^3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz" + integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.7.0" + unpipe "~1.0.0" + +raw-body@~1.1.0: + version "1.1.7" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz" + integrity sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg== + dependencies: + bytes "1" + string_decoder "0.10" + rc@^1.2.7: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" @@ -12569,7 +12503,7 @@ react-is@^18.0.0: resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react@*, "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", react@^18.2.0: +react@^18.2.0: version "18.3.1" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -12589,25 +12523,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.2.2, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^3.4.0: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -12686,7 +12602,9 @@ redis@3.1.2: redis-parser "^3.0.0" redlock@^5.0.0-beta.2: - version "v5.0.0-beta.2" + version "5.0.0-beta.2" + resolved "https://registry.yarnpkg.com/redlock/-/redlock-5.0.0-beta.2.tgz#a629c07e07d001c0fdd9f2efa614144c4416fe44" + integrity sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw== dependencies: node-abort-controller "^3.0.1" @@ -12905,14 +12823,7 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== -rimraf@^2.6.1: - version "2.7.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@^2.6.2: +rimraf@^2.6.1, rimraf@^2.6.2: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -12963,20 +12874,15 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-identifier@^0.4.1: version "0.4.2" @@ -13052,17 +12958,7 @@ secure-json-parse@^2.4.0: resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz" integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== -semver@^6.0.0: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^6.3.0: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^6.3.1: +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -13072,23 +12968,6 @@ semver@^7.1.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semve resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== -send@^1.1.0, send@^1.2.0: - version "1.2.1" - resolved "https://registry.npmjs.org/send/-/send-1.2.1.tgz" - integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== - dependencies: - debug "^4.4.3" - encodeurl "^2.0.0" - escape-html "^1.0.3" - etag "^1.8.1" - fresh "^2.0.0" - http-errors "^2.0.1" - mime-types "^3.0.2" - ms "^2.1.3" - on-finished "^2.4.1" - range-parser "^1.2.1" - statuses "^2.0.2" - send@0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" @@ -13127,6 +13006,23 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" +send@^1.1.0, send@^1.2.0: + version "1.2.1" + resolved "https://registry.npmjs.org/send/-/send-1.2.1.tgz" + integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + dependencies: + debug "^4.4.3" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.1" + mime-types "^3.0.2" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.2" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" @@ -13140,7 +13036,17 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@^1.14.1, serve-static@1.16.2: +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +serve-static@1.16.2, serve-static@^1.14.1: version "1.16.2" resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== @@ -13160,16 +13066,6 @@ serve-static@^2.2.0: parseurl "^1.3.3" send "^1.2.0" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" @@ -13216,16 +13112,16 @@ setimmediate@^1.0.4, setimmediate@^1.0.5: resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== -setprototypeof@~1.2.0, setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== +setprototypeof@1.2.0, setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + sha.js@^2.4.0, sha.js@^2.4.11: version "2.4.12" resolved "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz" @@ -13398,19 +13294,11 @@ sonic-boom@^3.0.0, sonic-boom@^3.7.0: dependencies: atomic-sleep "^1.0.0" -source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0": +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== -source-map-support@^0.5.12: - version "0.5.21" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-support@0.5.13: version "0.5.13" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" @@ -13419,6 +13307,14 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@^0.5.12: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map@^0.5.3: version "0.5.7" resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" @@ -13518,25 +13414,20 @@ static-eval@2.0.2: dependencies: escodegen "^1.8.1" -statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz" - integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== - -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -statuses@~1.5.0: +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== stop-iteration-iterator@^1.1.0: version "1.1.0" @@ -13573,25 +13464,6 @@ stream-to-array@^2.3.0: dependencies: any-promise "^1.1.0" -string_decoder@^1.1.1, string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -string_decoder@0.10: - version "0.10.31" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== - string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -13669,6 +13541,25 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +string_decoder@0.10: + version "0.10.31" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + stringify-object@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz" @@ -13721,7 +13612,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^2.0.0: +strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== @@ -13731,11 +13622,6 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - strnum@^1.1.1: version "1.1.2" resolved "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz" @@ -13778,14 +13664,7 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.1.1: +supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -13982,13 +13861,6 @@ tiny-lr@^1.1.1: object-assign "^4.1.0" qs "^6.4.0" -tiny-secp256k1@^2.2.3: - version "2.2.4" - resolved "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz" - integrity sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q== - dependencies: - uint8array-tools "0.0.7" - tiny-secp256k1@2.2.2: version "2.2.2" resolved "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.2.tgz" @@ -14003,10 +13875,12 @@ tiny-secp256k1@2.2.3: dependencies: uint8array-tools "0.0.7" -tmp@^0.2.1: - version "0.2.5" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz" - integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== +tiny-secp256k1@^2.2.3: + version "2.2.4" + resolved "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz" + integrity sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q== + dependencies: + uint8array-tools "0.0.7" tmp@0.2.1: version "0.2.1" @@ -14015,6 +13889,11 @@ tmp@0.2.1: dependencies: rimraf "^3.0.0" +tmp@^0.2.1: + version "0.2.5" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" @@ -14036,7 +13915,7 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@~1.0.1, toidentifier@1.0.1: +toidentifier@1.0.1, toidentifier@~1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== @@ -14121,7 +14000,7 @@ ts-node-dev@^2.0.0: ts-node "^10.4.0" tsconfig "^7.0.0" -ts-node@^10.4.0, ts-node@^10.9.1, ts-node@>=9.0.0: +ts-node@^10.4.0, ts-node@^10.9.1: version "10.9.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== @@ -14140,17 +14019,7 @@ ts-node@^10.4.0, ts-node@^10.9.1, ts-node@>=9.0.0: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tsconfig-paths@^3.10.1: - version "3.15.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" - integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tsconfig-paths@^3.15.0: +tsconfig-paths@^3.10.1, tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== @@ -14230,7 +14099,7 @@ twilio@^4.17.0: url-parse "^1.5.9" xmlbuilder "^13.0.2" -type-check@^0.4.0: +type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== @@ -14244,18 +14113,16 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - type-detect@4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@4.3.2: + version "4.3.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.3.2.tgz" + integrity sha512-VpwuOgnTsQUUWi0id8Hl4/xiQ+OoaeJGe8dnFjzubJYe/lOc2/d1Qx/d3FqWR0FlpOG/cvukAXfB12A49Y4iiA== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" @@ -14266,11 +14133,6 @@ type-fest@^0.21.3: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-fest@4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.3.2.tgz" - integrity sha512-VpwuOgnTsQUUWi0id8Hl4/xiQ+OoaeJGe8dnFjzubJYe/lOc2/d1Qx/d3FqWR0FlpOG/cvukAXfB12A49Y4iiA== - type-is@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz" @@ -14348,30 +14210,20 @@ typeforce@^1.11.3, typeforce@^1.11.5, typeforce@^1.18.0: resolved "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== -typescript@*, "typescript@^3.9.5 || ^4.9.5 || ^5", typescript@^5.2.2, typescript@>=2.7, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=4.2.0, typescript@>=4.7.2, typescript@>=5.0.0: - version "5.9.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== - -typescript@^3.9.10: +typescript@^3.9.10, typescript@^3.9.7: version "3.9.10" resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== -typescript@^3.9.7: - version "3.9.10" - resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz" - integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== - -typescript@^4.0.0: +typescript@^4.0.0, typescript@^4.9.5: version "4.9.5" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.2.2: + version "5.9.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" @@ -14421,16 +14273,16 @@ underscore.string@~3.3.5: sprintf-js "^1.1.1" util-deprecate "^1.0.2" -underscore@~1.13.2: - version "1.13.7" - resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz" - integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== - underscore@1.12.1: version "1.12.1" resolved "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== +underscore@~1.13.2: + version "1.13.7" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + undici-types@~6.21.0: version "6.21.0" resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" @@ -14493,7 +14345,7 @@ unixify@1.0.0: dependencies: normalize-path "^2.1.1" -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -14564,22 +14416,12 @@ uuid@^10.0.0: resolved "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz" integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== -uuid@^8.0.0: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -uuid@^8.3.2: +uuid@^8.0.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -uuid@^9.0.1: +uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -14640,17 +14482,17 @@ validate.io-number@^1.0.3: resolved "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz" integrity sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg== -value-or-promise@^1.0.12: - version "1.0.12" - resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz" - integrity sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q== - value-or-promise@1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz" integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg== -varuint-bitcoin@^1.1.2, varuint-bitcoin@1.1.2: +value-or-promise@^1.0.12: + version "1.0.12" + resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz" + integrity sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q== + +varuint-bitcoin@1.1.2, varuint-bitcoin@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz" integrity sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw== @@ -14674,7 +14516,7 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -wcwidth@^1.0.1, wcwidth@>=1.0.1: +wcwidth@>=1.0.1, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== @@ -14907,6 +14749,11 @@ write-file-atomic@^5.0.1: imurmurhash "^0.1.4" signal-exit "^4.0.1" +ws@8.14.2: + version "8.14.2" + resolved "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== + ws@^3.2.0: version "3.3.3" resolved "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz" @@ -14921,11 +14768,6 @@ ws@^8.14.1: resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== -ws@8.14.2: - version "8.14.2" - resolved "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz" - integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== - xml@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz" @@ -15040,7 +14882,7 @@ zod-to-json-schema@^3.25.0: resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz" integrity sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA== -zod@^3.0.0, zod@^3.22.0, zod@^3.22.2, "zod@^3.25 || ^4", "zod@^3.25 || ^4.0": +zod@^3.22.0, zod@^3.22.2, "zod@^3.25 || ^4.0": version "3.25.76" resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==