├── .gitignore
├── LICENSE
├── README.md
├── aws.tf
├── cloudfront.tf
├── collector
├── .gitignore
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── bin
│ ├── close-batch
│ ├── console
│ ├── insert-status
│ ├── merge-batch
│ ├── setup
│ └── store-status
├── collector.gemspec
├── entrypoint.rb
└── lib
│ └── publikes
│ ├── batch.rb
│ ├── close_batch_action.rb
│ ├── current.rb
│ ├── determine_mergeability_action.rb
│ ├── environment.rb
│ ├── errors.rb
│ ├── ingest_endpoint.rb
│ ├── insert_status_action.rb
│ ├── lambda_handler.rb
│ ├── lock.rb
│ ├── merge_batch_action.rb
│ └── store_status_action.rb
├── iam_lambda.tf
├── iam_scheduler.tf
├── iam_states.tf
├── lambda.tf
├── outputs.tf
├── s3.tf
├── scheduler.tf
├── secret.tf
├── sfn-rotate-batch.jsonnet
├── sfn-store-status.jsonnet
├── sfn.tf
├── sqs.tf
├── ui
├── .env
├── .eslintrc.cjs
├── .gitignore
├── deploy.rb
├── index.html
├── package-lock.json
├── package.json
├── src
│ ├── Api.ts
│ ├── App.css
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
├── variables.tf
└── versions.tf
/.gitignore:
--------------------------------------------------------------------------------
1 | *.zip
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Sorah Fukumori
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Publikes
2 |
3 | Publish your Twitter likes in a cheap serverless manner. Uses AWS Lambda and Amazon S3 for collection and storage.
4 |
5 | - Example: https://like.sorah.jp
6 |
7 | ## Setup
8 |
9 | ### Prerequisite
10 |
11 | - AWS account
12 | - Terraform
13 | - [jrsonnet](https://github.com/CertainLach/jrsonnet)
14 | - Node.JS (npm)
15 | - Ruby
16 |
17 | ### Infrastructure
18 |
19 | Deploy infrastructure using Terraform. This repository works as a terraform module:
20 |
21 | ```terraform
22 | module "prd" {
23 | source = "github.com/sorah/publikes"
24 |
25 | iam_role_prefix = "PublikesPrd"
26 | name_prefix = "publikes-prd"
27 | s3_bucket_name = "..."
28 | app_domain = "like.example.com"
29 |
30 | certificate_arn = data.aws_acm_certificate.my-certificate.arn
31 | cloudfront_log_bucket = "example.s3.amazonaws.com"
32 | cloudfront_log_prefix = "like.example.com/"
33 | }
34 |
35 | # outputs:
36 | # - module.prd.cloudfront_distribution_domain_name to create a DNS record
37 | # - module.prd.cloudfront_distribution_id for deploy.rb (see below)
38 | # - module.prd.lambda_function_url for ingestion webhook (see below)
39 |
40 | ```
41 |
42 | ### Web UI
43 |
44 | ```
45 | cd ui/
46 | npm i
47 | vim .env.production.local
48 | npm run build
49 | ruby deploy.rb $S3_BUCKET_NAME $CLOUDFRONT_DISTRIBUTION_ID
50 | ```
51 |
52 | Refer to [ui/.env](./ui/.env) for available environment variables.
53 |
54 | ### Secret
55 |
56 | You need to manually setup AWS Secrets Manager Secret `{name_prefix}/secret` with following key-value values:
57 |
58 | - `ingest_secret`: Secret string used for webhook. Send as `x-secret` in webhook requests to verify its authenticity.
59 |
60 | ### Liked Tweets Ingestion
61 |
62 | To avoid paying 100 USD/mo for Twitter API, this system uses IFTTT to feed likes information to store. You can use [New liked tweet by you] trigger with [Make a web request] action.
63 |
64 | - URL: `{lambda_function_uri}/publikes-ingest`
65 | - Content-Type: application/json
66 | - Headers: `x-secret: {secret}`
67 | - Content: `{"url": "<<<{{LinkToTweet}}>>>"}`
68 |
69 | ## How it works
70 |
71 | ### Ingestion
72 |
73 | The lambda function behind Function URL enqueues incoming tweet URL to the SQS queue. Then the messages will be consumed by an another Lambda function, and the included Tweet URLs are inserted into the latest page.
74 |
75 | ### Page Rotation
76 |
77 | The page of tweet IDs are rotated every N tweets (`MAX_ITEMS_IN_HEAD`) or after 6 hours using [rotate-batch] Step Functions State Machine. This system carefully uses S3 for ingestion not to happen any data loss by ingesting into the single object (such as race condition and conflicts). Partial data are eventually completed, especially during the merge process of [rotate-batch] state machine run.
78 |
79 | ## License
80 |
81 | MIT License
82 |
83 |
--------------------------------------------------------------------------------
/aws.tf:
--------------------------------------------------------------------------------
1 | data "aws_region" "current" {}
2 | data "aws_caller_identity" "current" {}
3 | data "aws_default_tags" "current" {}
4 |
--------------------------------------------------------------------------------
/cloudfront.tf:
--------------------------------------------------------------------------------
1 | data "aws_cloudfront_origin_request_policy" "Managed-CORS-S3Origin" {
2 | name = "Managed-CORS-S3Origin"
3 | }
4 | data "aws_cloudfront_cache_policy" "Managed-CachingDisabled" {
5 | name = "Managed-CachingDisabled"
6 | }
7 |
8 | data "aws_cloudfront_cache_policy" "Managed-CachingOptimized" {
9 | name = "Managed-CachingOptimized"
10 | }
11 |
12 | # NOTE: UseOriginCacheControlHeaders includes Host header forwarding which we don't want to.
13 | #data "aws_cloudfront_cache_policy" "UseOriginCacheControlHeaders" {
14 | # name = "UseOriginCacheControlHeaders"
15 | #}
16 | resource "aws_cloudfront_cache_policy" "data" {
17 | name = "${var.name_prefix}-data"
18 | comment = "${var.name_prefix} CachingOptimized + min=0"
19 | default_ttl = 0
20 | max_ttl = 31536000 # 1 yr
21 | min_ttl = 0
22 | parameters_in_cache_key_and_forwarded_to_origin {
23 | enable_accept_encoding_brotli = true
24 | enable_accept_encoding_gzip = true
25 |
26 | cookies_config {
27 | cookie_behavior = "none"
28 | }
29 | headers_config {
30 | header_behavior = "none"
31 | }
32 | query_strings_config {
33 | query_string_behavior = "none"
34 | }
35 | }
36 | }
37 |
38 | resource "aws_cloudfront_distribution" "public" {
39 | enabled = true
40 | is_ipv6_enabled = true
41 | http_version = "http2and3"
42 | comment = "publikes/${var.name_prefix}"
43 | aliases = [var.app_domain]
44 |
45 | viewer_certificate {
46 | acm_certificate_arn = var.certificate_arn
47 | minimum_protocol_version = "TLSv1.2_2021"
48 | ssl_support_method = "sni-only"
49 | }
50 |
51 | logging_config {
52 | include_cookies = false
53 | bucket = var.cloudfront_log_bucket
54 | prefix = var.cloudfront_log_prefix
55 | }
56 |
57 | origin {
58 | origin_id = "s3public-ui"
59 | domain_name = aws_s3_bucket.bucket.bucket_regional_domain_name
60 | origin_path = "/ui"
61 | }
62 |
63 | origin {
64 | origin_id = "s3public-data"
65 | domain_name = aws_s3_bucket.bucket.bucket_regional_domain_name
66 | }
67 |
68 | default_root_object = "index.html"
69 |
70 | ordered_cache_behavior {
71 | path_pattern = "/data/*"
72 |
73 | allowed_methods = ["GET", "HEAD", "OPTIONS", ]
74 | cached_methods = ["GET", "HEAD"]
75 |
76 | target_origin_id = "s3public-data"
77 | cache_policy_id = aws_cloudfront_cache_policy.data.id
78 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.Managed-CORS-S3Origin.id
79 |
80 | compress = true
81 | viewer_protocol_policy = "redirect-to-https"
82 | }
83 |
84 | default_cache_behavior {
85 | allowed_methods = ["GET", "HEAD", "OPTIONS", ]
86 | cached_methods = ["GET", "HEAD"]
87 |
88 | target_origin_id = "s3public-ui"
89 | cache_policy_id = data.aws_cloudfront_cache_policy.Managed-CachingOptimized.id
90 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.Managed-CORS-S3Origin.id
91 |
92 | compress = true
93 | viewer_protocol_policy = "redirect-to-https"
94 | }
95 |
96 | restrictions {
97 | geo_restriction {
98 | restriction_type = "none"
99 | }
100 | }
101 |
102 | tags = {
103 | Name = "${var.name_prefix}"
104 | Component = "cloudfront"
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/collector/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 |
10 | # rspec failure tracking
11 | .rspec_status
12 |
--------------------------------------------------------------------------------
/collector/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Specify your gem's dependencies in collector.gemspec
6 | gemspec
7 |
8 | gem "rake", "~> 13.0"
9 |
10 | gem "rspec", "~> 3.0"
11 |
12 | gem 'nokogiri'
13 |
--------------------------------------------------------------------------------
/collector/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | publikes-collector (0.0.1)
5 | aws-sdk-s3
6 | aws-sdk-secretsmanager
7 | aws-sdk-sqs
8 | aws-sdk-states
9 |
10 | GEM
11 | remote: https://rubygems.org/
12 | specs:
13 | aws-eventstream (1.3.0)
14 | aws-partitions (1.944.0)
15 | aws-sdk-core (3.197.0)
16 | aws-eventstream (~> 1, >= 1.3.0)
17 | aws-partitions (~> 1, >= 1.651.0)
18 | aws-sigv4 (~> 1.8)
19 | jmespath (~> 1, >= 1.6.1)
20 | aws-sdk-kms (1.84.0)
21 | aws-sdk-core (~> 3, >= 3.197.0)
22 | aws-sigv4 (~> 1.1)
23 | aws-sdk-s3 (1.152.3)
24 | aws-sdk-core (~> 3, >= 3.197.0)
25 | aws-sdk-kms (~> 1)
26 | aws-sigv4 (~> 1.8)
27 | aws-sdk-secretsmanager (1.97.0)
28 | aws-sdk-core (~> 3, >= 3.197.0)
29 | aws-sigv4 (~> 1.1)
30 | aws-sdk-sqs (1.76.0)
31 | aws-sdk-core (~> 3, >= 3.197.0)
32 | aws-sigv4 (~> 1.1)
33 | aws-sdk-states (1.24.0)
34 | aws-sdk-core (~> 3, >= 3.71.0)
35 | aws-sigv4 (~> 1.1)
36 | aws-sigv4 (1.8.0)
37 | aws-eventstream (~> 1, >= 1.0.2)
38 | diff-lcs (1.5.1)
39 | jmespath (1.6.2)
40 | mini_portile2 (2.8.7)
41 | nokogiri (1.16.6)
42 | mini_portile2 (~> 2.8.2)
43 | racc (~> 1.4)
44 | racc (1.8.0)
45 | rake (13.2.1)
46 | rspec (3.13.0)
47 | rspec-core (~> 3.13.0)
48 | rspec-expectations (~> 3.13.0)
49 | rspec-mocks (~> 3.13.0)
50 | rspec-core (3.13.0)
51 | rspec-support (~> 3.13.0)
52 | rspec-expectations (3.13.1)
53 | diff-lcs (>= 1.2.0, < 2.0)
54 | rspec-support (~> 3.13.0)
55 | rspec-mocks (3.13.1)
56 | diff-lcs (>= 1.2.0, < 2.0)
57 | rspec-support (~> 3.13.0)
58 | rspec-support (3.13.1)
59 |
60 | PLATFORMS
61 | ruby
62 |
63 | DEPENDENCIES
64 | nokogiri
65 | publikes-collector!
66 | rake (~> 13.0)
67 | rspec (~> 3.0)
68 |
69 | BUNDLED WITH
70 | 2.4.22
71 |
--------------------------------------------------------------------------------
/collector/README.md:
--------------------------------------------------------------------------------
1 | # Publikes Collector
2 |
3 | ## bin/insert-batch
4 |
5 | 1. fail if `data/private/locks/current` exists
6 | 1. put `data/private/locks/current` with `lock_id`=`bin/insert-batch/{hostname}/{now}/{random}`
7 | 1. check `data/private/locks/current` has expected content, otherwise fail
8 | 1. Get `data/public/current.json`
9 | 1. Update `data/public/batches/{new_last_id}.json` with `next`=`{current_last_id}`
10 | 1. Update `data/public/current.json` with `last`=`{new_last_id}`
11 | 1. Delete `data/private/locks/current`
12 |
13 | ## Step Functions state machines
14 |
15 | ### rotate-batch
16 |
17 | 1. fail if `data/private/locks/current` exists
18 | 1. put `data/private/locks/current` with `lock_id`=`sfn/rotate-batch/{sfn_id}`
19 | 1. check `data/private/locks/current` has expected content, otherwise fail
20 | 1. trigger __close_batch__
21 | 1. delete `data/private/locks/current`
22 | 1. wait 2 min
23 | 1. trigger __merge_batch__
24 |
25 | ## store-status
26 |
27 | 1. trigger __store_status__
28 |
29 | ## Lambda function actions
30 |
31 | ### publikes_action=store_status | bin/store-status
32 |
33 | いちおうかいた
34 |
35 | Input: `status_id`
36 |
37 | 1. Merge `data/private/statuses/{id}.json` with `fxtwitter_data` and set `complete`=true
38 |
39 | ### publikes_action=close_batch | bin/close-batch
40 |
41 | いちおうかいた
42 |
43 | Input: None, Output: `current_was`, `current`, `closed_head_id`, `new_head_id`
44 |
45 | 1. TODO: Perform lock in state machine
46 | 1. Get `data/public/current.json` for `closing_head_id`
47 | 1. Create `data/public/batches/{new_head_id}.json` with `head`=true,`next`=`{closing_head_id}`
48 | 1. Update `data/public/current.json` with `last`=`{closing_head_id}`,`head`=`{new_head_id}`
49 | 1. Return `{closing_head_id}` and `{new_head_id}`
50 |
51 | ### publikes_action=merge_batch | bin/merge-batch
52 |
53 | いちおうかいた
54 |
55 | Input: `batch_id`, Output: `batch_id`
56 |
57 | 1. Get `data/public/batches/{batch_id}.json`
58 | 1. Read all pages by written order (`data/public/pages/head/{batch_id}/{page}.json`) and merge into larger pages
59 | 1. List prefix `data/public/pages/head/{batch_id}/` and merge missing items into the new first page
60 | 1. Put pages into `data/public/pages/merged/{batch_id}/{page}.json`
61 | 1. Replace `data/public/batches/{batch_id}.json` with new pages
62 | 1. Delete `data/public/pages/head/{batch_id}/*`
63 |
64 | ### `requestContext` (object) - Process function URL request
65 |
66 | いちおうかいた
67 |
68 | 1. Validate secret
69 | 1. Insert request into FIFO SQS queue
70 | - `id`
71 | - `ts`
72 | 1. Return 20x Accepted
73 |
74 | ### `Records` (array) | bin/insert-status
75 |
76 | いちおうかいた
77 |
78 | Input: `status_ids`
79 |
80 | 1. Read `data/public/current.json` for `head_batch_id`
81 | 1. Get `data/public/statuses/{status_id}.json` and return if it exists
82 | 1. Get `data/public/batches/{head_batch_id}.json`
83 | 1. Put `data/public/pages/head/{head_batch_id}/{status_id}.json`
84 | 1. Update `data/public/batches/{head_batch_id}.json`
85 | 1. Trigger store_status state machine
86 |
--------------------------------------------------------------------------------
/collector/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 |
6 | RSpec::Core::RakeTask.new(:spec)
7 |
8 | task default: :spec
9 |
--------------------------------------------------------------------------------
/collector/bin/close-batch:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | $stdout.sync = true
5 |
6 | require 'json'
7 | require 'publikes/environment'
8 | require 'publikes/lock'
9 | require 'publikes/close_batch_action'
10 |
11 | environment = Publikes::Environment.from_os
12 |
13 | lock = Publikes::Lock.new(environment:, group: 'current', id: Publikes::Lock.id_by_hostname('bin/close-batch'))
14 | lock.with_lock do
15 | p Publikes::CloseBatchAction.new(
16 | environment:,
17 | ).perform
18 | end
19 |
--------------------------------------------------------------------------------
/collector/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "collector"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | require "irb"
11 | IRB.start(__FILE__)
12 |
--------------------------------------------------------------------------------
/collector/bin/insert-status:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | $stdout.sync = true
5 |
6 | require 'json'
7 | require 'publikes/environment'
8 | require 'publikes/insert_status_action'
9 | environment = Publikes::Environment.from_os
10 |
11 | p Publikes::InsertStatusAction.new(
12 | environment:,
13 | statuses: ARGV.map do |id|
14 | {
15 | id:,
16 | ts: Time.now.to_i,
17 | }
18 | end,
19 | ).perform
20 |
--------------------------------------------------------------------------------
/collector/bin/merge-batch:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | $stdout.sync = true
5 |
6 | require 'json'
7 | require 'publikes/environment'
8 | require 'publikes/lock'
9 | require 'publikes/merge_batch_action'
10 |
11 | environment = Publikes::Environment.from_os
12 |
13 | p Publikes::MergeBatchAction.new(
14 | environment:,
15 | batch_id: ARGV[0],
16 | ).perform
17 |
--------------------------------------------------------------------------------
/collector/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/collector/bin/store-status:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | $stdout.sync = true
5 |
6 | require 'json'
7 | require 'publikes/environment'
8 | require 'publikes/store_status_action'
9 | environment = Publikes::Environment.from_os
10 |
11 | ARGV.each do |id|
12 | p id
13 | p Publikes::StoreStatusAction.new(
14 | environment:,
15 | status_id: id,
16 | ).perform
17 | end
18 |
--------------------------------------------------------------------------------
/collector/collector.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "publikes-collector"
5 | spec.version = "0.0.1"
6 | spec.authors = ["Sorah Fukumori"]
7 | spec.email = ["her@sorah.jp"]
8 |
9 | spec.summary = "publikes collector"
10 | spec.license = "MIT"
11 | spec.required_ruby_version = ">= 3.2.0"
12 |
13 | spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
14 |
15 | # Specify which files should be added to the gem when it is released.
16 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17 | spec.files = Dir.chdir(__dir__) do
18 | `git ls-files -z`.split("\x0").reject do |f|
19 | (File.expand_path(f) == __FILE__) ||
20 | f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
21 | end
22 | end
23 | spec.bindir = "exe"
24 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
25 | spec.require_paths = ["lib"]
26 |
27 | # Uncomment to register a new dependency of your gem
28 | spec.add_dependency "aws-sdk-s3"
29 | spec.add_dependency "aws-sdk-secretsmanager"
30 | spec.add_dependency "aws-sdk-sqs"
31 | spec.add_dependency "aws-sdk-states"
32 |
33 | # For more information and examples about making a new gem, check out our
34 | # guide at: https://bundler.io/guides/creating_gem.html
35 | end
36 |
--------------------------------------------------------------------------------
/collector/entrypoint.rb:
--------------------------------------------------------------------------------
1 | $:.unshift(File.join(__dir__, 'lib'))
2 | require 'publikes/lambda_handler'
3 |
--------------------------------------------------------------------------------
/collector/lib/publikes/batch.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'json'
3 | require 'publikes/errors'
4 |
5 | module Publikes
6 | module Batch
7 | def self.key(id)
8 | "data/public/batches/#{id}.json"
9 | end
10 |
11 | def self.make_id(suffix=nil)
12 | "#{(Time.now.to_f*10000).round}#{suffix}"
13 | end
14 |
15 | def self.empty(id:, head: true, next: nil)
16 | ts = Time.now.to_i
17 | {
18 | id:,
19 | head:,
20 | pages: [],
21 | next:,
22 | created_at: ts,
23 | updated_at: ts,
24 | }
25 | end
26 |
27 | def self.get(id, env:)
28 | batch = JSON.parse(
29 | env.s3.get_object(
30 | bucket: env.s3_bucket,
31 | key: key(id),
32 | ).body.read,
33 | symbolize_names: true,
34 | )
35 | raise "batch_id inconsistency #{id.inspect}, #{batch[:id].inspect}" unless id == batch[:id]
36 | batch
37 | end
38 |
39 | def self.put(batch, env:, verify: true, cache_control: "public, max-age=0")
40 | if verify
41 | nonce = SecureRandom.urlsafe_base64(32)
42 | batch[:update_nonce] = nonce
43 | end
44 | batch[:updated_at] = Time.now.to_i
45 | retval = env.s3.put_object(
46 | bucket: env.s3_bucket,
47 | key: key(batch.fetch(:id)),
48 | content_type: "application/json; charset=utf-8",
49 | cache_control:,
50 | body: JSON.generate(batch),
51 | )
52 | if verify
53 | sleep(rand(3000)/1000.0)
54 | updated_batch = get(batch[:id], env:)
55 | raise Publikes::Errors::NeedRetry if updated_batch[:update_nonce] != nonce
56 | end
57 | retval
58 | end
59 |
60 | def self.mergeable?(batch, env:)
61 | batch[:head] && batch[:pages].size >= env.max_items_in_head
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/collector/lib/publikes/close_batch_action.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'json'
3 | require 'aws-sdk-s3'
4 |
5 | require 'publikes/current'
6 | require 'publikes/batch'
7 |
8 | module Publikes
9 | class CloseBatchAction
10 | def initialize(environment:)
11 | @environment = environment
12 |
13 | @current = Publikes::Current.new(environment:)
14 | end
15 |
16 | def env; @environment; end
17 | attr_reader :current
18 |
19 | def perform
20 | closing_head_id = @current.value[:head]
21 | new_head_id = Publikes::Batch.make_id('-auto')
22 |
23 | env.s3.put_object(
24 | bucket: env.s3_bucket,
25 | key: "data/public/batches/#{new_head_id}.json",
26 | content_type: "application/json; charset=utf-8",
27 | body: JSON.generate(Publikes::Batch.empty(id: new_head_id, head: true, next: closing_head_id)),
28 | )
29 |
30 | @current.update(
31 | head: new_head_id,
32 | last: closing_head_id,
33 | )
34 |
35 | {
36 | current: @current.value,
37 | current_was: @current.value_was,
38 | closed_head_id: closing_head_id,
39 | new_head_id:,
40 | }
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/collector/lib/publikes/current.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'json'
3 | require 'aws-sdk-s3'
4 |
5 | module Publikes
6 | class Current
7 | KEY = "data/public/current.json"
8 |
9 | def initialize(environment:)
10 | @environment = environment
11 | @value = nil
12 | @value_was = nil
13 | end
14 |
15 | def env; @environment; end
16 |
17 | def value
18 | @value ||= begin
19 | JSON.parse(
20 | env.s3.get_object(
21 | bucket: env.s3_bucket,
22 | key: KEY,
23 | ).body.read,
24 | symbolize_names: true,
25 | )
26 | rescue Aws::S3::Errors::NoSuchKey
27 | {
28 | head: nil,
29 | last: nil,
30 | updated_at: 0,
31 | }
32 | end
33 | end
34 |
35 | def value_was
36 | @value_was
37 | end
38 |
39 | def update(hash)
40 | new_value = value.merge(hash)
41 | new_value[:updated_at] = Time.now.to_i
42 | env.s3.put_object(
43 | bucket: env.s3_bucket,
44 | key: KEY,
45 | content_type: "application/json; charset=utf-8",
46 | cache_control: "public, s-maxage=300, max-age=0",
47 | body: JSON.generate(new_value),
48 | )
49 | @value_was = value
50 | @value = new_value
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/collector/lib/publikes/determine_mergeability_action.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'json'
3 | require 'aws-sdk-s3'
4 |
5 | require 'publikes/current'
6 | require 'publikes/batch'
7 |
8 | module Publikes
9 | class DetermineMergeabilityAction
10 | def initialize(environment:)
11 | @environment = environment
12 |
13 | @current = Publikes::Current.new(environment:)
14 | end
15 |
16 | def env; @environment; end
17 | attr_reader :current
18 |
19 | def perform
20 | batch_id = @current.value[:head]
21 | unless batch_id
22 | return { mergeability: false, batch_id: }
23 | end
24 |
25 | batch = Publikes::Batch.get(batch_id, env:)
26 | raise "head #{batch_id.inspect} is not head" unless batch[:head]
27 |
28 | return {
29 | batch_id:,
30 | mergeable: Publikes::Batch.mergeable?(batch, env:),
31 | }
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/collector/lib/publikes/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'logger'
3 | require 'json'
4 | require 'aws-sdk-s3'
5 | require 'aws-sdk-secretsmanager'
6 | require 'aws-sdk-sqs'
7 | require 'aws-sdk-states'
8 |
9 | module Publikes
10 | class Environment
11 | def initialize(s3_bucket:, sqs_queue_url: nil, secret_id: nil, state_machine_arn_store_status: nil, state_machine_arn_rotate_batch: nil, max_items_per_page: nil, max_items_in_head: nil)
12 | @s3_bucket = s3_bucket
13 | @sqs_queue_url = sqs_queue_url
14 | @secret_id = secret_id
15 | @state_machine_arn_store_status = state_machine_arn_store_status
16 | @state_machine_arn_rotate_batch = state_machine_arn_rotate_batch
17 |
18 | @max_items_per_page = max_items_per_page || 10
19 | @max_items_in_head = max_items_in_head || 20
20 |
21 | @s3 = nil
22 | @secretsmanager = nil
23 | @sqs = nil
24 | @states = nil
25 |
26 | @secret = nil
27 |
28 | @logger = Logger.new($stdout)
29 | end
30 |
31 | def self.from_os
32 | new(
33 | s3_bucket: ENV.fetch('S3_BUCKET'),
34 | sqs_queue_url: ENV['SQS_QUEUE_URL'],
35 | secret_id: ENV['SECRET_ID'],
36 | state_machine_arn_store_status: ENV['STATE_MACHINE_ARN_STORE_STATUS'],
37 | state_machine_arn_rotate_batch: ENV['STATE_MACHINE_ARN_ROTATE_BATCH'],
38 | max_items_per_page: ENV['MAX_ITEMS_PER_PAGE']&.to_i,
39 | max_items_in_head: ENV['MAX_ITEMS_IN_HEAD']&.to_i,
40 | )
41 | end
42 |
43 | attr_reader :s3_bucket
44 | attr_reader :sqs_queue_url
45 | attr_reader :secret_id
46 | attr_reader :state_machine_arn_store_status
47 | attr_reader :state_machine_arn_rotate_batch
48 |
49 | attr_reader :max_items_per_page
50 | attr_reader :max_items_in_head
51 |
52 | def s3
53 | @s3 ||= Aws::S3::Client.new(logger:)
54 | end
55 |
56 | def secretsmanager
57 | @secretsmanager ||= Aws::SecretsManager::Client.new(logger:)
58 | end
59 |
60 | def sqs
61 | @sqs ||= Aws::SQS::Client.new(logger:)
62 | end
63 |
64 | def states
65 | @states ||= Aws::States::Client.new(logger:)
66 | end
67 |
68 | def secret
69 | @secret ||= JSON.parse(secretsmanager.get_secret_value(secret_id:).secret_string)
70 | end
71 |
72 | def logger
73 | @logger
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/collector/lib/publikes/errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Publikes
3 | class Errors
4 | class NeedRetry < StandardError; end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/collector/lib/publikes/ingest_endpoint.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'aws-sdk-sqs'
3 | require 'openssl'
4 | require 'json'
5 |
6 | module Publikes
7 | class IngestEndpoint
8 | def initialize(environment:, event:)
9 | @environment = environment
10 | @event = event
11 | @request = Request.from_fn_url_event(event)
12 | @meta = {}
13 | end
14 |
15 | attr_reader :request
16 |
17 | Request = Struct.new(:method, :path, :query, :headers, :body, keyword_init: true) do
18 | def self.from_fn_url_event(event)
19 | body = event['isBase64Encoded'] ? event.fetch('body').unpack1('m*') : event.fetch('body', '')
20 | new(
21 | method: event.dig('requestContext', 'http', 'method'),
22 | path: event.dig('requestContext', 'http', 'path'),
23 | query: event.fetch('queryStringParameters', {}),
24 | headers: event.fetch('headers'),
25 | body: body,
26 | )
27 | end
28 |
29 | def content_type
30 | headers['content-type']
31 | end
32 |
33 | def json
34 | raise Error.new(400, 'not a json request') unless content_type&.match?(%r{\Aapplication/json(?:;.*)?\z})
35 | @json ||= JSON.parse(body)
36 | rescue JSON::ParserError => e
37 | raise Error.new(400, e.inspect)
38 | end
39 | end
40 |
41 | Response = Struct.new(:status, :headers, :body, :meta, keyword_init: true) do
42 | def as_json
43 | {
44 | 'cookies' => [],
45 | 'isBase64Encoded' => false,
46 | 'statusCode' => status,
47 | 'headers' => headers,
48 | 'body' => body,
49 | }
50 | end
51 | end
52 |
53 |
54 | class Error < StandardError
55 | def initialize(code, message)
56 | super(message)
57 | @code = code
58 | end
59 |
60 | def as_response
61 | Response.new(status: code, headers: { 'content-type' => 'application/json; charset=utf-8' }, body: "#{JSON.generate({ok: false, error: {code: code, message: message}})}\n", meta: {error: self.inspect, cause: self.cause&.inspect})
62 | end
63 |
64 | attr_reader :code
65 | end
66 |
67 |
68 | def respond
69 | begin
70 | begin
71 | ta = Time.now
72 | respond_inner()
73 | rescue NoMemoryError, ScriptError, SecurityError, SignalException, SystemExit, SystemStackError => e
74 | raise e
75 | rescue Error => e
76 | raise e
77 | rescue Exception => e
78 | $stderr.puts e.full_message
79 | raise Error.new(500, 'Internal Server Error')
80 | end
81 | rescue Error => e
82 | e.as_response
83 | end.tap do |response|
84 | puts JSON.generate(
85 | status: response.status,
86 | method: request.method,
87 | path: request.path,
88 | query: request.query.reject { |k,v| k.start_with?('secure_') },
89 | reqtime: Time.now.to_f - ta.to_f,
90 | meta: @meta.merge(response.meta || {}),
91 | )
92 | end.as_json
93 | end
94 |
95 | def respond_inner
96 | case [request.method, request.path]
97 | when ['POST', '/publikes-ingest']
98 | authorize!
99 | handle_ingest
100 | else
101 | Error.new(404, 'not found').as_response
102 | end
103 | end
104 |
105 | def authorize!
106 | unless OpenSSL.secure_compare(@environment.secret.fetch('ingest_secret'), request.headers['x-secret'] || '')
107 | raise Error.new(401, 'Unauthorized')
108 | end
109 | end
110 |
111 | def handle_ingest
112 | url = request.json['url']
113 | raise Error.new(400, 'missing url') if !url.is_a?(String) || url.empty?
114 |
115 | m = url.match(%r{\Ahttps?://(?:twitter|x)\.com/[^/]+/status(?:es)?/(\d+)})
116 | raise Error.new(400, 'invalid url') unless m
117 | id = m[1]
118 | puts(JSON.generate(action: 'send_message', url:, id:))
119 | @environment.sqs.send_message(
120 | queue_url: @environment.sqs_queue_url,
121 | message_body: JSON.generate({id:, ts: Time.now.to_i}),
122 | )
123 |
124 | Response.new(status: 200, headers: {'content-type' => 'application/json; charset=utf-8'}, body: '{"ok": true}', meta: {})
125 | end
126 |
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/collector/lib/publikes/insert_status_action.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'json'
3 | require 'aws-sdk-s3'
4 |
5 | require 'publikes/current'
6 | require 'publikes/batch'
7 | require 'publikes/errors'
8 |
9 | module Publikes
10 | class InsertStatusAction
11 | def initialize(environment:, statuses:, timestamp: Time.now.to_i)
12 | @environment = environment
13 | @statuses = statuses # [{id:, ts:}], old to new
14 |
15 | @current = Publikes::Current.new(environment:)
16 | @timestamp = timestamp
17 | end
18 |
19 | def env; @environment; end
20 | attr_reader :statuses
21 | attr_reader :current
22 | attr_reader :timestamp
23 |
24 | def perform
25 | retval = nil
26 |
27 | begin
28 | retval = perform_inner()
29 | rescue Publikes::Errors::NeedRetry => e
30 | env.logger.warn(e.inspect)
31 | sleep(rand(5000)/1000.0)
32 | retry
33 | end
34 |
35 | if env.state_machine_arn_store_status
36 | retval[:new_statuses].each do |s|
37 | env.states.start_execution(
38 | state_machine_arn: env.state_machine_arn_store_status,
39 | name: "status-#{s.fetch(:id)}",
40 | input: JSON.generate({status_id: s.fetch(:id)}),
41 | )
42 | rescue Aws::States::Errors::ExecutionAlreadyExists
43 | end
44 | end
45 |
46 | if env.state_machine_arn_rotate_batch && retval[:mergeable]
47 | begin
48 | env.states.start_execution(
49 | state_machine_arn: env.state_machine_arn_rotate_batch,
50 | input: JSON.generate({auto: true}),
51 | )
52 | rescue Aws::States::Errors::ExecutionAlreadyExists
53 | end
54 | end
55 |
56 | retval
57 | end
58 |
59 | def perform_inner
60 | batch_id = current.value[:head] || Publikes::Batch.make_id('-auto')
61 | unless current.value[:head]
62 | current.update(head: batch_id)
63 | sleep(rand(3000)/1000.0)
64 | raise Publikes::Errors::NeedRetry if Publikes::Current.new(environment: env).value[:head] != batch_id
65 | end
66 |
67 | new_statuses = statuses.reject do |s|
68 | env.s3.head_object(bucket: env.s3_bucket, key: "data/private/statuses/#{s.fetch(:id)}.json")
69 | rescue Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::NotFound
70 | nil
71 | end
72 |
73 | if new_statuses.empty?
74 | return {new_statuses:, batch_id:, mergeable: false}
75 | end
76 |
77 | batch = begin
78 | Publikes::Batch.get(batch_id, env:)
79 | rescue Aws::S3::Errors::NoSuchKey
80 | Publikes::Batch.empty(id: batch_id, head: true, next: nil)
81 | end
82 |
83 | new_pages = new_statuses.map do |s|
84 | page_id = "head/#{batch_id}/#{s.fetch(:id)}"
85 | env.s3.put_object(
86 | bucket: env.s3_bucket,
87 | key: "data/public/pages/#{page_id}.json",
88 | content_type: "application/json; charset=utf-8",
89 | body: JSON.generate({
90 | id: page_id,
91 | statuses: [s],
92 | created_at: timestamp,
93 | }),
94 | )
95 | page_id
96 | end
97 |
98 | # new to old
99 | batch[:pages].reverse! # old to new
100 | batch[:pages].push(*new_pages) # old to new
101 | batch[:pages].uniq! # old to new
102 | batch[:pages].reverse! # new to old
103 |
104 | Publikes::Batch.put(batch, env:)
105 |
106 | {
107 | new_statuses:,
108 | batch_id:,
109 | mergeable: Publikes::Batch.mergeable?(batch, env:)
110 | }
111 | end
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/collector/lib/publikes/lambda_handler.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | $stdout.sync = true
3 |
4 | require 'publikes/environment'
5 | require 'publikes/close_batch_action'
6 | require 'publikes/determine_mergeability_action'
7 | require 'publikes/ingest_endpoint'
8 | require 'publikes/insert_status_action'
9 | require 'publikes/merge_batch_action'
10 | require 'publikes/store_status_action'
11 |
12 | require 'json'
13 |
14 | module Publikes
15 | module LambdaHandler
16 | def self.sqs_handler(event:, context:)
17 | Publikes::InsertStatusAction.new(
18 | environment:,
19 | statuses: event.fetch('Records').map do |r|
20 | body = JSON.parse(r.fetch('body'))
21 | puts(JSON.generate(sqs_record: body))
22 | {
23 | id: body.fetch('id'),
24 | ts: body.fetch('ts'),
25 | }
26 | end,
27 | ).perform
28 | end
29 |
30 | def self.http_handler(event:, context:)
31 | Publikes::IngestEndpoint.new(environment:, event:).respond
32 | end
33 |
34 | def self.action_handler(event:, context:)
35 | case event.fetch('publikes_action')
36 | when 'store_status'
37 | store_status(event:, context:)
38 | when 'determine_mergeability'
39 | determine_mergeability(event:, context:)
40 | when 'close_batch'
41 | close_batch(event:, context:)
42 | when 'merge_batch'
43 | merge_batch(event:, context:)
44 | end
45 | end
46 |
47 | def self.store_status(event:, context:)
48 | Publikes::StoreStatusAction.new(
49 | environment:,
50 | status_id: event.fetch('status_id'),
51 | ).perform
52 | end
53 |
54 | def self.determine_mergeability(event:, context:)
55 | Publikes::DetermineMergeabilityAction.new(
56 | environment:,
57 | ).perform
58 | end
59 |
60 | def self.close_batch(event:, context:)
61 | Publikes::CloseBatchAction.new(
62 | environment:,
63 | ).perform
64 | end
65 |
66 | def self.merge_batch(event:, context:)
67 | Publikes::MergeBatchAction.new(
68 | environment:,
69 | batch_id: event.fetch('batch_id'),
70 | ).perform
71 | end
72 |
73 | def self.environment
74 | @environment ||= Publikes::Environment.from_os
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/collector/lib/publikes/lock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'json'
3 | require 'securerandom'
4 | require 'aws-sdk-s3'
5 |
6 | module Publikes
7 | class Lock
8 | class Occupied < StandardError; end
9 |
10 | def self.id_by_hostname(prefix)
11 | "#{prefix}/#{ENV['HOSTNAME'] || Socket.gethostname}/#{$$}/#{SecureRandom.urlsafe_base64(32)}"
12 | end
13 |
14 | def initialize(environment:, group:, id:)
15 | @environment = environment
16 | @group = group
17 | @id = id
18 |
19 | @key = nil
20 | end
21 |
22 | def env; @environment; end
23 | attr_reader :group
24 | attr_reader :id
25 |
26 | def key
27 | @key ||= "data/private/locks/#{group}"
28 | end
29 |
30 | def lock
31 | v = get(); raise Occupied, "#{key.inspect} is already occupied by #{v.inspect}" if v[:lock_id] && v[:lock_id] != id
32 | if v[:lock_id] == id
33 | env.logger.info "Lock: (already locked) #{key.inspect} = #{v.inspect}"
34 | return v
35 | end
36 |
37 | v = {lock_id: id}
38 | env.s3.put_object(
39 | bucket: env.s3_bucket,
40 | key:,
41 | content_type: "application/json; charset=utf-8",
42 | body: JSON.generate(v),
43 | )
44 | v = get(); raise Occupied, "Attempted to lock #{key.inspect} but occupied by #{v.inspect}" if v[:lock_id] && v[:lock_id] != id
45 | env.logger.info "Lock: (placed) #{key.inspect} = #{v.inspect}"
46 | v
47 | end
48 |
49 | def unlock
50 | v = get(); raise Occupied, "#{key.inspect} is not occupied by us (#{v.inspect})" if v[:lock_id] != id
51 | env.logger.info "Lock: (released) #{key.inspect} = #{v.inspect}"
52 | env.s3.delete_object(
53 | bucket: env.s3_bucket,
54 | key:,
55 | )
56 | nil
57 | end
58 |
59 | def with_lock
60 | lock
61 | yield
62 | ensure
63 | begin
64 | unlock
65 | rescue Occupied => e
66 | env.logger.warn "Lock#with_lock: ensure unlock failure: #{e.inspect}"
67 | end
68 | end
69 |
70 | private def get
71 | begin
72 | JSON.parse(
73 | env.s3.get_object(
74 | bucket: env.s3_bucket,
75 | key:,
76 | ).body.read,
77 | symbolize_names: true,
78 | )
79 | rescue Aws::S3::Errors::NoSuchKey
80 | {}
81 | end
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/collector/lib/publikes/merge_batch_action.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'json'
3 | require 'aws-sdk-s3'
4 |
5 | require 'publikes/current'
6 | require 'publikes/batch'
7 |
8 | module Publikes
9 | class MergeBatchAction
10 | def initialize(environment:, batch_id:, timestamp: Time.now.to_i)
11 | @environment = environment
12 | @batch_id = batch_id
13 | @timestamp = timestamp
14 | end
15 |
16 | def env; @environment; end
17 | attr_reader :batch_id
18 | attr_reader :timestamp
19 |
20 | def perform
21 | batch = Publikes::Batch.get(batch_id, env:)
22 | raise "head=false, already merged?" unless batch[:head]
23 |
24 | # Merge pages
25 | item_ids = {}
26 | pending_items = []
27 | merged_pages = []
28 | batch.fetch(:pages).each do |page_id|
29 | page = JSON.parse(env.s3.get_object(bucket: env.s3_bucket, key: "data/public/pages/#{page_id}.json").body.read, symbolize_names: true)
30 | pending_items.concat(page.fetch(:statuses))
31 | if pending_items.size >= env.max_items_per_page
32 | merged_pages.push(create_page(merged_pages.size.succ, pending_items, item_ids))
33 | pending_items = []
34 | end
35 | end
36 | merged_pages.push(create_page(merged_pages.size.succ, pending_items, item_ids)) unless pending_items.empty?
37 |
38 | # Detect and collect missing items in batch
39 | missing_items = env.s3.list_objects_v2(bucket: env.s3_bucket, prefix: "data/public/pages/head/#{batch_id}/").flat_map(&:contents).reject do |content|
40 | id = content.key.split(?/).last.split(?.,2).first # head pages are numbered using status_id
41 | item_ids[id]
42 | end.flat_map do |missing_content|
43 | page = JSON.parse(env.s3.get_object(bucket: env.s3_bucket, key: missing_content.key).body.read, symbolize_names: true)
44 | page.fetch(:statuses)
45 | end
46 | merged_pages.unshift(create_page(0, missing_items, item_ids)) unless missing_items.empty?
47 |
48 | # Replace batch with merged pages
49 | Publikes::Batch.put(
50 | batch.merge(
51 | head: false,
52 | pages: merged_pages,
53 | ),
54 | cache_control: 'public, max-age=604800',
55 | env:,
56 | )
57 |
58 | # Delete head items
59 | env.s3.list_objects_v2(bucket: env.s3_bucket, prefix: "data/public/pages/head/#{batch_id}/").flat_map(&:contents).each do |content|
60 | env.s3.delete_object(bucket: env.s3_bucket, key: content.key)
61 | end
62 |
63 | {
64 | batch_id:
65 | }
66 | end
67 |
68 | private def create_page(pagenum, statuses, item_ids)
69 | id = "merged/#{batch_id}/#{'%06d' % pagenum}"
70 | env.s3.put_object(
71 | bucket: env.s3_bucket,
72 | key: "data/public/pages/#{id}.json",
73 | content_type: "application/json; charset=utf-8",
74 | cache_control: "public, max-age=604800",
75 | body: JSON.generate(
76 | id:,
77 | statuses:,
78 | created_at: timestamp,
79 | )
80 | )
81 | statuses.each do |s|
82 | item_ids[s[:id].to_s] = true
83 | end
84 | id
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/collector/lib/publikes/store_status_action.rb:
--------------------------------------------------------------------------------
1 | require 'json'
2 | require 'open-uri'
3 | require 'aws-sdk-s3'
4 |
5 | module Publikes
6 | class StoreStatusAction
7 | def initialize(environment:, status_id:)
8 | @environment = environment
9 | @status_id = status_id.to_s
10 |
11 | raise ArgumentError, "invalid status_id" unless @status_id.match?(/\A[0-9a-zA-Z]+\z/)
12 | end
13 |
14 | attr_reader :status_id
15 | def env; @environment; end
16 |
17 | USER_AGENT = 'Publikes-Crawler (+https://github.com/sorah/publikes)'
18 |
19 | def perform
20 | key = "data/private/statuses/#{status_id}.json"
21 | data = begin
22 | JSON.parse(
23 | env.s3.get_object(
24 | bucket: env.s3_bucket,
25 | key:,
26 | ).body.read,
27 | symbolize_names: true,
28 | )
29 | rescue Aws::S3::Errors::NoSuchKey
30 | {
31 | id: status_id.to_s,
32 | complete: false,
33 | graphql_data: nil,
34 | fxtwitter_data: nil,
35 | saved_at: nil,
36 | }
37 | end
38 |
39 | fxtwitter_data = begin
40 | JSON.parse(URI.open("https://api.fxtwitter.com/status/#{status_id}", "User-Agent" => USER_AGENT, &:read))
41 | rescue OpenURI::HTTPError => e
42 | code = e.io.status[0]
43 | raise unless code == '404' || code == '403' || code == '401'
44 | end
45 |
46 | new_data = data.merge(
47 | complete: true,
48 | saved_at: Time.now.to_i,
49 | fxtwitter_data: fxtwitter_data || data[:fxtwitter_data],
50 | )
51 |
52 | env.s3.put_object(
53 | bucket: env.s3_bucket,
54 | key:,
55 | content_type: "application/json; charset=utf-8",
56 | body: JSON.generate(new_data),
57 | )
58 |
59 | {
60 | status_id:,
61 | }
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/iam_lambda.tf:
--------------------------------------------------------------------------------
1 | resource "aws_iam_role" "Lambda" {
2 | name = "${var.iam_role_prefix}Lambda"
3 | description = "publikes ${var.name_prefix} Lambda"
4 | assume_role_policy = data.aws_iam_policy_document.Lambda-trust.json
5 | max_session_duration = 3600
6 | }
7 |
8 | data "aws_iam_policy_document" "Lambda-trust" {
9 | statement {
10 | effect = "Allow"
11 | actions = ["sts:AssumeRole"]
12 | principals {
13 | type = "Service"
14 | identifiers = [
15 | "lambda.amazonaws.com",
16 | ]
17 | }
18 | }
19 | }
20 |
21 | resource "aws_iam_role_policy" "Lambda" {
22 | role = aws_iam_role.Lambda.name
23 | policy = data.aws_iam_policy_document.Lambda.json
24 | }
25 |
26 | data "aws_iam_policy_document" "Lambda" {
27 | statement {
28 | effect = "Allow"
29 | actions = ["secretsmanager:GetSecretValue"]
30 | resources = [
31 | aws_secretsmanager_secret.secret.arn,
32 | ]
33 | }
34 | statement {
35 | effect = "Allow"
36 | actions = [
37 | "s3:ListBucket",
38 | ]
39 | resources = [
40 | aws_s3_bucket.bucket.arn,
41 | ]
42 | }
43 | statement {
44 | effect = "Allow"
45 | actions = [
46 | "s3:GetObject",
47 | "s3:PutObject",
48 | ]
49 | resources = [
50 | "${aws_s3_bucket.bucket.arn}/data/*",
51 | ]
52 | }
53 | statement {
54 | effect = "Allow"
55 | actions = [
56 | "s3:DeleteObject",
57 | ]
58 | resources = [
59 | "${aws_s3_bucket.bucket.arn}/data/public/pages/head/*",
60 | "${aws_s3_bucket.bucket.arn}/data/private/locks/*",
61 | ]
62 | }
63 | }
64 |
65 | resource "aws_iam_role_policy" "Lambda2" {
66 | role = aws_iam_role.Lambda.name
67 | policy = data.aws_iam_policy_document.Lambda2.json
68 | }
69 | data "aws_iam_policy_document" "Lambda2" {
70 | statement {
71 | effect = "Allow"
72 | actions = [
73 | "sqs:SendMessage",
74 |
75 | # Used by AWS control plane
76 | "sqs:ReceiveMessage",
77 | "sqs:DeleteMessage",
78 | "sqs:GetQueueAttributes",
79 | ]
80 | resources = [
81 | aws_sqs_queue.queue.arn,
82 | ]
83 | }
84 |
85 | statement {
86 | effect = "Allow"
87 | actions = [
88 | "states:StartExecution",
89 | ]
90 | resources = [
91 | aws_sfn_state_machine.store-status.arn,
92 | aws_sfn_state_machine.rotate-batch.arn,
93 | ]
94 | }
95 | }
96 |
97 | resource "aws_iam_role_policy_attachment" "function-AWSLambdaBasicExecutionRole" {
98 | role = aws_iam_role.Lambda.name
99 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
100 | }
101 |
--------------------------------------------------------------------------------
/iam_scheduler.tf:
--------------------------------------------------------------------------------
1 | resource "aws_iam_role" "Scheduler" {
2 | name = "${var.iam_role_prefix}Scheduler"
3 | description = "publikes ${var.name_prefix} Scheduler"
4 | assume_role_policy = data.aws_iam_policy_document.Scheduler-trust.json
5 | max_session_duration = 3600
6 | }
7 |
8 | data "aws_iam_policy_document" "Scheduler-trust" {
9 | statement {
10 | effect = "Allow"
11 | actions = ["sts:AssumeRole"]
12 | principals {
13 | type = "Service"
14 | identifiers = [
15 | "scheduler.amazonaws.com",
16 | ]
17 | }
18 | }
19 | }
20 |
21 | resource "aws_iam_role_policy" "Scheduler" {
22 | role = aws_iam_role.Scheduler.name
23 | policy = data.aws_iam_policy_document.Scheduler.json
24 | }
25 |
26 | data "aws_iam_policy_document" "Scheduler" {
27 | statement {
28 | effect = "Allow"
29 | actions = [
30 | "states:StartExecution",
31 | ]
32 | resources = [
33 | aws_sfn_state_machine.rotate-batch.arn,
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/iam_states.tf:
--------------------------------------------------------------------------------
1 | resource "aws_iam_role" "States" {
2 | name = "${var.iam_role_prefix}States"
3 | description = "publikes ${var.name_prefix} States"
4 | assume_role_policy = data.aws_iam_policy_document.States-trust.json
5 | max_session_duration = 3600
6 | }
7 |
8 | data "aws_iam_policy_document" "States-trust" {
9 | statement {
10 | effect = "Allow"
11 | actions = ["sts:AssumeRole"]
12 | principals {
13 | type = "Service"
14 | identifiers = [
15 | "states.amazonaws.com",
16 | ]
17 | }
18 | }
19 | }
20 |
21 | resource "aws_iam_role_policy" "States" {
22 | role = aws_iam_role.States.name
23 | policy = data.aws_iam_policy_document.States.json
24 | }
25 |
26 | data "aws_iam_policy_document" "States" {
27 | statement {
28 | effect = "Allow"
29 | actions = [
30 | "s3:ListBucket",
31 | ]
32 | resources = [
33 | aws_s3_bucket.bucket.arn,
34 | ]
35 | }
36 | statement {
37 | effect = "Allow"
38 | actions = [
39 | "s3:GetObject",
40 | "s3:PutObject",
41 | ]
42 | resources = [
43 | "${aws_s3_bucket.bucket.arn}/data/private/locks/*",
44 | ]
45 | }
46 | statement {
47 | effect = "Allow"
48 | actions = [
49 | "s3:DeleteObject",
50 | ]
51 | resources = [
52 | "${aws_s3_bucket.bucket.arn}/data/private/locks/*",
53 | ]
54 | }
55 | }
56 |
57 | resource "aws_iam_role_policy" "States2" {
58 | role = aws_iam_role.States.name
59 | policy = data.aws_iam_policy_document.States2.json
60 | }
61 | data "aws_iam_policy_document" "States2" {
62 | statement {
63 | effect = "Allow"
64 | actions = [
65 | "lambda:InvokeFunction"
66 | ]
67 | resources = [
68 | aws_lambda_function.collector-action.arn,
69 | ]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lambda.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | lambda_env_vars = merge({
3 | S3_BUCKET = aws_s3_bucket.bucket.bucket
4 | SECRET_ID = aws_secretsmanager_secret.secret.arn
5 | }, var.lambda_env_vars)
6 | }
7 |
8 | data "archive_file" "collector" {
9 | type = "zip"
10 | source_dir = "${path.module}/collector"
11 | output_path = "${path.module}/collector.zip"
12 | }
13 |
14 | resource "aws_lambda_function" "collector-action" {
15 | function_name = "${var.name_prefix}-collector-action"
16 |
17 | filename = "${path.module}/collector.zip"
18 | source_code_hash = data.archive_file.collector.output_base64sha256
19 | handler = "entrypoint.Publikes::LambdaHandler.action_handler"
20 | runtime = "ruby3.2"
21 | architectures = ["arm64"]
22 |
23 | role = aws_iam_role.Lambda.arn
24 |
25 | memory_size = 256
26 | timeout = 60 * 15
27 |
28 | environment {
29 | variables = merge(local.lambda_env_vars, {
30 | })
31 | }
32 | }
33 |
34 | resource "aws_lambda_function" "collector-sqs" {
35 | function_name = "${var.name_prefix}-collector-sqs"
36 |
37 | filename = "${path.module}/collector.zip"
38 | source_code_hash = data.archive_file.collector.output_base64sha256
39 | handler = "entrypoint.Publikes::LambdaHandler.sqs_handler"
40 | runtime = "ruby3.2"
41 | architectures = ["arm64"]
42 |
43 | role = aws_iam_role.Lambda.arn
44 |
45 | memory_size = 256
46 | timeout = 30
47 |
48 | environment {
49 | variables = merge(local.lambda_env_vars, {
50 | STATE_MACHINE_ARN_STORE_STATUS = aws_sfn_state_machine.store-status.arn,
51 | STATE_MACHINE_ARN_ROTATE_BATCH = aws_sfn_state_machine.rotate-batch.arn, #"arn:aws:states:${data.aws_region.current.id}:${data.aws_caller_identity.current.id}:stateMachine:${var.name_prefix}-rotate-batch"
52 | })
53 | }
54 | }
55 | resource "aws_lambda_event_source_mapping" "collector-sqs" {
56 | event_source_arn = aws_sqs_queue.queue.arn
57 | function_name = aws_lambda_function.collector-sqs.arn
58 |
59 | batch_size = 10
60 | maximum_batching_window_in_seconds = 60
61 |
62 | scaling_config {
63 | maximum_concurrency = 2
64 | }
65 | }
66 |
67 | resource "aws_lambda_function" "collector-http" {
68 | function_name = "${var.name_prefix}-collector-http"
69 |
70 | filename = "${path.module}/collector.zip"
71 | source_code_hash = data.archive_file.collector.output_base64sha256
72 | handler = "entrypoint.Publikes::LambdaHandler.http_handler"
73 | runtime = "ruby3.2"
74 | architectures = ["arm64"]
75 |
76 | role = aws_iam_role.Lambda.arn
77 |
78 | memory_size = 256
79 | timeout = 15
80 |
81 | environment {
82 | variables = merge(local.lambda_env_vars, {
83 | SQS_QUEUE_URL = aws_sqs_queue.queue.url
84 | })
85 | }
86 | }
87 | resource "aws_lambda_function_url" "collector-http" {
88 | function_name = aws_lambda_function.collector-http.function_name
89 | authorization_type = "NONE"
90 | }
91 |
--------------------------------------------------------------------------------
/outputs.tf:
--------------------------------------------------------------------------------
1 | output "cloudfront_distribution_domain_name" {
2 | value = aws_cloudfront_distribution.public.domain_name
3 | }
4 |
5 | output "cloudfront_distribution_id" {
6 | value = aws_cloudfront_distribution.public.id
7 | }
8 |
9 | output "lambda_function_url" {
10 | value = aws_lambda_function_url.collector-http.function_url
11 | }
12 |
--------------------------------------------------------------------------------
/s3.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "bucket" {
2 | bucket = var.s3_bucket_name
3 | }
4 | resource "aws_s3_bucket_public_access_block" "bucket" {
5 | bucket = aws_s3_bucket.bucket.id
6 |
7 | block_public_acls = false
8 | block_public_policy = false
9 | ignore_public_acls = false
10 | restrict_public_buckets = false
11 | }
12 | resource "aws_s3_bucket_policy" "bucket" {
13 | bucket = aws_s3_bucket.bucket.id
14 | policy = data.aws_iam_policy_document.bucket.json
15 |
16 | depends_on = [aws_s3_bucket_public_access_block.bucket]
17 | }
18 | data "aws_iam_policy_document" "bucket" {
19 | statement {
20 | effect = "Allow"
21 | actions = ["s3:GetObject"]
22 | resources = [
23 | "${aws_s3_bucket.bucket.arn}/data/public/*",
24 | "${aws_s3_bucket.bucket.arn}/ui/*",
25 | ]
26 | principals {
27 | type = "AWS"
28 | identifiers = [
29 | "*",
30 | ]
31 | }
32 | }
33 | }
34 | resource "aws_s3_bucket_versioning" "bucket" {
35 | bucket = aws_s3_bucket.bucket.id
36 | versioning_configuration {
37 | status = "Enabled"
38 | }
39 | }
40 |
41 | resource "aws_s3_bucket_lifecycle_configuration" "bucket" {
42 | depends_on = [aws_s3_bucket_versioning.bucket]
43 |
44 | bucket = aws_s3_bucket.bucket.id
45 |
46 | rule {
47 | id = "config"
48 | status = "Enabled"
49 |
50 | filter {
51 | }
52 |
53 | noncurrent_version_expiration {
54 | noncurrent_days = 7
55 | }
56 | }
57 | }
58 |
59 | resource "aws_s3_bucket_cors_configuration" "bucket" {
60 | bucket = aws_s3_bucket.bucket.id
61 |
62 | cors_rule {
63 | allowed_methods = ["GET"]
64 | allowed_origins = ["*"]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/scheduler.tf:
--------------------------------------------------------------------------------
1 | resource "aws_scheduler_schedule" "rotate-batch" {
2 | name = "${var.name_prefix}-rotate-batch"
3 |
4 | flexible_time_window {
5 | mode = "FLEXIBLE"
6 | maximum_window_in_minutes = 20
7 | }
8 |
9 | schedule_expression = "rate(6 hours)"
10 |
11 | target {
12 | arn = "arn:aws:scheduler:::aws-sdk:sfn:startExecution"
13 | role_arn = aws_iam_role.Scheduler.arn
14 |
15 | input = jsonencode({
16 | StateMachineArn = aws_sfn_state_machine.rotate-batch.arn
17 | Input = jsonencode({})
18 | })
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/secret.tf:
--------------------------------------------------------------------------------
1 | resource "aws_secretsmanager_secret" "secret" {
2 | name = "${var.name_prefix}/secret"
3 | }
4 |
--------------------------------------------------------------------------------
/sfn-rotate-batch.jsonnet:
--------------------------------------------------------------------------------
1 | local tfstate = std.parseJson(std.extVar('TFSTATE'));
2 |
3 | local lambdaRetry = {
4 | ErrorEquals: [
5 | 'Lambda.ServiceException',
6 | 'Lambda.AWSLambdaException',
7 | 'Lambda.SdkClientException',
8 | 'Lambda.TooManyRequestsException',
9 | ],
10 | IntervalSeconds: 1,
11 | BackoffRate: 2,
12 | MaxAttempts: 20,
13 | JitterStrategy: 'FULL',
14 | };
15 |
16 | local definition = {
17 | StartAt: 'InvokeDetermineMergeability',
18 | States: {
19 | InvokeDetermineMergeability: {
20 | Type: 'Task',
21 | Resource: 'arn:aws:states:::lambda:invoke',
22 | Parameters: {
23 | FunctionName: tfstate.lambda_arn_action,
24 | Payload: {
25 | publikes_action: 'determine_mergeability',
26 | },
27 | },
28 | ResultSelector: {
29 | 'result.$': '$.Payload',
30 | },
31 | ResultPath: '$.mergeability',
32 | Next: 'TestMergeability',
33 | },
34 | TestMergeability: {
35 | Type: 'Choice',
36 | Choices: [
37 | {
38 | Not: {
39 | Variable: '$.mergeability.result.mergeable',
40 | BooleanEquals: true,
41 | },
42 | Next: 'Skip',
43 | },
44 | ],
45 | Default: 'CheckExistingLock',
46 | },
47 | Skip: {
48 | Type: 'Succeed',
49 | },
50 | CheckExistingLock: {
51 | Type: 'Task',
52 | Parameters: {
53 | Bucket: tfstate.s3_bucket,
54 | Key: 'data/private/locks/current',
55 | },
56 | Resource: 'arn:aws:states:::aws-sdk:s3:getObject',
57 | Catch: [
58 | {
59 | ErrorEquals: [
60 | 'States.TaskFailed',
61 | ],
62 | Next: 'SetLockId',
63 | },
64 | ],
65 | Next: 'LockOccupied',
66 | },
67 | SetLockId: {
68 | Type: 'Pass',
69 | Parameters: {
70 | lock: {
71 | 'lock_id.$': "States.Format('sfn/rotate-batch/{}/{}', $$.Execution.Id, States.UUID())",
72 | },
73 | },
74 | Next: 'PlaceLock',
75 | },
76 | PlaceLock: {
77 | Type: 'Task',
78 | Parameters: {
79 | Bucket: tfstate.s3_bucket,
80 | Key: 'data/private/locks/current',
81 | ContentType: 'application/json',
82 | 'Body.$': '$.lock', // automatically JSON encoded
83 | },
84 | Resource: 'arn:aws:states:::aws-sdk:s3:putObject',
85 | ResultPath: null,
86 | Next: 'RetrieveLockOwnership',
87 | },
88 | RetrieveLockOwnership: {
89 | Type: 'Task',
90 | Parameters: {
91 | Bucket: tfstate.s3_bucket,
92 | Key: 'data/private/locks/current',
93 | },
94 | Resource: 'arn:aws:states:::aws-sdk:s3:getObject',
95 | ResultPath: '$.possibly_acquired_lock',
96 | ResultSelector: {
97 | 'body.$': 'States.StringToJson($.Body)',
98 | },
99 | Retry: [
100 | {
101 | ErrorEquals: [
102 | 'States.TaskFailed',
103 | ],
104 | BackoffRate: 2,
105 | IntervalSeconds: 2,
106 | MaxAttempts: 8,
107 | JitterStrategy: 'FULL',
108 | },
109 | ],
110 | Next: 'VerifyLockOwnership',
111 | },
112 | VerifyLockOwnership: {
113 | Type: 'Choice',
114 | Choices: [
115 | {
116 | Not: {
117 | Variable: '$.possibly_acquired_lock.body.lock_id',
118 | StringEqualsPath: '$.lock.lock_id',
119 | },
120 | Next: 'LockOccupied',
121 | },
122 | ],
123 | Default: 'InvokeCloseBatch',
124 | },
125 | LockOccupied: {
126 | Type: 'Succeed',
127 | },
128 | InvokeCloseBatch: {
129 | Type: 'Task',
130 | Resource: 'arn:aws:states:::lambda:invoke',
131 | Parameters: {
132 | FunctionName: tfstate.lambda_arn_action,
133 | Payload: {
134 | publikes_action: 'close_batch',
135 | },
136 | },
137 | Retry: [lambdaRetry],
138 | ResultSelector: {
139 | 'result.$': '$.Payload',
140 | },
141 | ResultPath: '$.closed_batch',
142 | Next: 'ReleaseLock',
143 | },
144 | ReleaseLock: {
145 | Type: 'Task',
146 | Parameters: {
147 | Bucket: tfstate.s3_bucket,
148 | Key: 'data/private/locks/current',
149 | },
150 | Resource: 'arn:aws:states:::aws-sdk:s3:deleteObject',
151 | Retry: [
152 | {
153 | ErrorEquals: [
154 | 'States.TaskFailed',
155 | ],
156 | BackoffRate: 2,
157 | IntervalSeconds: 2,
158 | MaxAttempts: 8,
159 | JitterStrategy: 'FULL',
160 | },
161 | ],
162 | ResultPath: null,
163 | Next: 'Wait',
164 | },
165 | Wait: {
166 | Type: 'Wait',
167 | Seconds: 120,
168 | Next: 'InvokeMergeBatch',
169 | },
170 | InvokeMergeBatch: {
171 | Type: 'Task',
172 | Resource: 'arn:aws:states:::lambda:invoke',
173 | Parameters: {
174 | FunctionName: tfstate.lambda_arn_action,
175 | Payload: {
176 | publikes_action: 'merge_batch',
177 | 'batch_id.$': '$.closed_batch.result.closed_head_id',
178 | },
179 | },
180 | Retry: [lambdaRetry],
181 | ResultSelector: {
182 | 'result.$': '$.Payload',
183 | },
184 | ResultPath: '$.merge',
185 | End: true,
186 | },
187 | },
188 | };
189 | { definition: std.manifestJsonEx(definition, ' ') }
190 |
--------------------------------------------------------------------------------
/sfn-store-status.jsonnet:
--------------------------------------------------------------------------------
1 | local tfstate = std.parseJson(std.extVar('TFSTATE'));
2 |
3 | local definition = {
4 | StartAt: 'InvokeStoreStatus',
5 | States: {
6 | InvokeStoreStatus: {
7 | Type: 'Task',
8 | Resource: 'arn:aws:states:::lambda:invoke',
9 | OutputPath: '$.Payload',
10 | Parameters: {
11 | Payload: {
12 | publikes_action: 'store_status',
13 | 'status_id.$': '$.status_id',
14 | },
15 | FunctionName: tfstate.lambda_arn_action,
16 | },
17 | Retry: [
18 | {
19 | ErrorEquals: [
20 | 'Lambda.ServiceException',
21 | 'Lambda.AWSLambdaException',
22 | 'Lambda.SdkClientException',
23 | 'Lambda.TooManyRequestsException',
24 | ],
25 | IntervalSeconds: 1,
26 | BackoffRate: 2,
27 | MaxAttempts: 20,
28 | JitterStrategy: 'FULL',
29 | },
30 | ],
31 | End: true,
32 | },
33 | },
34 | };
35 |
36 |
37 | { definition: std.manifestJsonEx(definition, ' ') }
38 |
--------------------------------------------------------------------------------
/sfn.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | sfn_tfstate = {
3 | s3_bucket = aws_s3_bucket.bucket.bucket
4 | lambda_arn_action = aws_lambda_function.collector-action.arn
5 | }
6 | }
7 |
8 | data "external" "sfn-store-status" {
9 | program = ["jrsonnet", "--ext-str", "TFSTATE=${jsonencode(local.sfn_tfstate)}", "${path.module}/sfn-store-status.jsonnet"]
10 | }
11 | resource "aws_sfn_state_machine" "store-status" {
12 | name = "${var.name_prefix}-store-status"
13 | role_arn = aws_iam_role.States.arn
14 | definition = data.external.sfn-store-status.result.definition
15 | }
16 |
17 | data "external" "sfn-rotate-batch" {
18 | program = ["jrsonnet", "--ext-str", "TFSTATE=${jsonencode(local.sfn_tfstate)}", "${path.module}/sfn-rotate-batch.jsonnet"]
19 | }
20 | moved {
21 | from = aws_sfn_state_machine.store-rotate-batch
22 | to = aws_sfn_state_machine.rotate-batch
23 | }
24 | resource "aws_sfn_state_machine" "rotate-batch" {
25 | name = "${var.name_prefix}-rotate-batch"
26 | role_arn = aws_iam_role.States.arn
27 | definition = data.external.sfn-rotate-batch.result.definition
28 | }
29 |
--------------------------------------------------------------------------------
/sqs.tf:
--------------------------------------------------------------------------------
1 | resource "aws_sqs_queue" "queue" {
2 | name = var.name_prefix
3 |
4 | visibility_timeout_seconds = 60 + (30 * 6)
5 |
6 | redrive_policy = jsonencode({
7 | deadLetterTargetArn = aws_sqs_queue.dlq.arn
8 | maxReceiveCount = 10
9 | })
10 | }
11 |
12 | resource "aws_sqs_queue" "dlq" {
13 | name = "${var.name_prefix}-dlq"
14 |
15 | visibility_timeout_seconds = 60
16 | }
17 | resource "aws_sqs_queue_redrive_allow_policy" "dlq" {
18 | queue_url = aws_sqs_queue.dlq.id
19 | redrive_allow_policy = jsonencode({
20 | redrivePermission = "byQueue",
21 | sourceQueueArns = [aws_sqs_queue.queue.arn]
22 | })
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/ui/.env:
--------------------------------------------------------------------------------
1 | VITE_HTML_LANG=ja
2 | VITE_APP_LANG=en
3 | VITE_HTML_TITLE=Publikes
4 | VITE_DATA_URL=/data/public
5 | VITE_HTML_HEAD=
6 |
--------------------------------------------------------------------------------
/ui/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/ui/deploy.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'aws-sdk-s3'
3 | require 'fileutils'
4 | require 'securerandom'
5 | require 'digest/md5'
6 | require 'logger'
7 | require 'thread'
8 |
9 | $stdout.sync = true
10 |
11 | CACHE_CONTROLS = {
12 | 'font/woff2' => 'max-age=31536000',
13 | 'text/css; charset=utf-8' => 'max-age=31536000',
14 | 'text/javascript; charset=utf-8' => 'max-age=31536000',
15 | 'text/plain; charset=utf-8' => 'public, must-revalidate, max-age=0, s-maxage=0',
16 | 'text/html; charset=utf-8' => 'max-age=0, s-maxage=31536000',
17 | 'application/json; charset=utf-8' => 'public, must-revalidate, max-age=0, s-maxage=0',
18 | 'image/webp' => 'public, must-revalidate, max-age=0, s-maxage=0',
19 | 'image/svg+xml' => 'public, must-revalidate, max-age=0, s-maxage=0',
20 | }
21 |
22 | bucket = ARGV[0]
23 | cloudfront_distribution_id = ARGV[1]
24 | prefix ="ui/"
25 | @s3 = Aws::S3::Client.new(logger: Logger.new($stdout))
26 |
27 | abort "usage: #$0 bucket [cloudfornt_distribution_id]" unless bucket
28 |
29 | srcdir = File.join(__dir__,"dist")
30 |
31 | publicdir = File.join(__dir__,"public")
32 | Dir[File.join(publicdir, "**", '*')].each do |path|
33 | key = "#{path[(publicdir.size + File::SEPARATOR.size)..-1].split(File::SEPARATOR).join('/')}"
34 | dst = File.join(srcdir,key)
35 | FileUtils.mkdir_p(File.dirname(dst))
36 | p [:cp, path, dst]
37 | File.write "#{dst}", File.read(path)
38 | end
39 |
40 | indexhtml = File.read(File.join(srcdir,'index.html'))
41 | %w(
42 | ).each do |path|
43 | dst = File.join(srcdir,path)
44 | FileUtils.mkdir_p(File.dirname(dst))
45 | File.write "#{dst}.html", indexhtml
46 | end
47 |
48 | Dir[File.join(srcdir, '**', '*')].each do |path|
49 | next if File.directory?(path)
50 | key = "#{prefix}#{path[(srcdir.size + File::SEPARATOR.size)..-1].split(File::SEPARATOR).join('/')}"
51 | .sub(/\.html$/,'')
52 |
53 | case path
54 | when File.join(srcdir, 'index.html')
55 | key = "#{prefix}index.html"
56 | end
57 |
58 | content_type = case path
59 | when /\.txt$/
60 | 'text/plain; charset=utf-8'
61 | when /\.html$/
62 | 'text/html; charset=utf-8'
63 | when /\.js$/
64 | 'text/javascript; charset=utf-8'
65 | when /\.css$/
66 | 'text/css; charset=utf-8'
67 | when /feed\.xml$/
68 | 'application/atom+xml; charset=utf-8'
69 | when /\.json$/
70 | 'application/json; charset=utf-8'
71 | when /\.woff2$/
72 | 'font/woff2'
73 | when /\.webp$/
74 | 'image/webp'
75 | when /\.svg$/
76 | 'image/svg+xml'
77 | end
78 |
79 | cache_control = CACHE_CONTROLS[content_type]
80 | File.open(path,'r') do |io|
81 | @s3.put_object(
82 | bucket: bucket,
83 | key: key,
84 | content_type: content_type,
85 | cache_control: cache_control,
86 | body: io,
87 | )
88 | end
89 | end
90 |
91 | if cloudfront_distribution_id
92 | require 'aws-sdk-cloudfront'
93 | @cf = Aws::CloudFront::Client.new(region: 'us-east-1', logger: Logger.new($stdout))
94 | resp = @cf.create_invalidation(
95 | distribution_id: cloudfront_distribution_id,
96 | invalidation_batch: {
97 | paths: {
98 | quantity: 2,
99 | items: ['/', '/index.html'],
100 | },
101 | caller_reference: ENV['GITHUB_ACTION'] ? "#{ENV['GITHUB_ACTION']}_#{ENV['GITHUB_RUN_ID']}" : SecureRandom.hex(10),
102 | },
103 | )
104 | @cf.wait_until(:invalidation_completed, { distribution_id: cloudfront_distribution_id, id: resp.invalidation.id })
105 | end
106 |
--------------------------------------------------------------------------------
/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %VITE_HTML_HEAD%
9 | %VITE_HTML_TITLE%
10 |
11 |
12 | Loading...
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/ui/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "publikes-ui",
3 | "version": "0.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "publikes-ui",
9 | "version": "0.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "react": "^18.2.0",
13 | "react-dom": "^18.2.0",
14 | "react-tweet": "^3.2.1",
15 | "swr": "^2.2.5",
16 | "wretch": "^2.9.0"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^18.2.66",
20 | "@types/react-dom": "^18.2.22",
21 | "@typescript-eslint/eslint-plugin": "^7.2.0",
22 | "@typescript-eslint/parser": "^7.2.0",
23 | "@vitejs/plugin-react-swc": "^3.5.0",
24 | "eslint": "^8.57.0",
25 | "eslint-plugin-react-hooks": "^4.6.0",
26 | "eslint-plugin-react-refresh": "^0.4.6",
27 | "typescript": "^5.2.2",
28 | "vite": "^5.2.0"
29 | }
30 | },
31 | "node_modules/@esbuild/aix-ppc64": {
32 | "version": "0.21.5",
33 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
34 | "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
35 | "cpu": [
36 | "ppc64"
37 | ],
38 | "dev": true,
39 | "optional": true,
40 | "os": [
41 | "aix"
42 | ],
43 | "engines": {
44 | "node": ">=12"
45 | }
46 | },
47 | "node_modules/@esbuild/android-arm": {
48 | "version": "0.21.5",
49 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
50 | "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
51 | "cpu": [
52 | "arm"
53 | ],
54 | "dev": true,
55 | "optional": true,
56 | "os": [
57 | "android"
58 | ],
59 | "engines": {
60 | "node": ">=12"
61 | }
62 | },
63 | "node_modules/@esbuild/android-arm64": {
64 | "version": "0.21.5",
65 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
66 | "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
67 | "cpu": [
68 | "arm64"
69 | ],
70 | "dev": true,
71 | "optional": true,
72 | "os": [
73 | "android"
74 | ],
75 | "engines": {
76 | "node": ">=12"
77 | }
78 | },
79 | "node_modules/@esbuild/android-x64": {
80 | "version": "0.21.5",
81 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
82 | "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
83 | "cpu": [
84 | "x64"
85 | ],
86 | "dev": true,
87 | "optional": true,
88 | "os": [
89 | "android"
90 | ],
91 | "engines": {
92 | "node": ">=12"
93 | }
94 | },
95 | "node_modules/@esbuild/darwin-arm64": {
96 | "version": "0.21.5",
97 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
98 | "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
99 | "cpu": [
100 | "arm64"
101 | ],
102 | "dev": true,
103 | "optional": true,
104 | "os": [
105 | "darwin"
106 | ],
107 | "engines": {
108 | "node": ">=12"
109 | }
110 | },
111 | "node_modules/@esbuild/darwin-x64": {
112 | "version": "0.21.5",
113 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
114 | "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
115 | "cpu": [
116 | "x64"
117 | ],
118 | "dev": true,
119 | "optional": true,
120 | "os": [
121 | "darwin"
122 | ],
123 | "engines": {
124 | "node": ">=12"
125 | }
126 | },
127 | "node_modules/@esbuild/freebsd-arm64": {
128 | "version": "0.21.5",
129 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
130 | "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
131 | "cpu": [
132 | "arm64"
133 | ],
134 | "dev": true,
135 | "optional": true,
136 | "os": [
137 | "freebsd"
138 | ],
139 | "engines": {
140 | "node": ">=12"
141 | }
142 | },
143 | "node_modules/@esbuild/freebsd-x64": {
144 | "version": "0.21.5",
145 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
146 | "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
147 | "cpu": [
148 | "x64"
149 | ],
150 | "dev": true,
151 | "optional": true,
152 | "os": [
153 | "freebsd"
154 | ],
155 | "engines": {
156 | "node": ">=12"
157 | }
158 | },
159 | "node_modules/@esbuild/linux-arm": {
160 | "version": "0.21.5",
161 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
162 | "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
163 | "cpu": [
164 | "arm"
165 | ],
166 | "dev": true,
167 | "optional": true,
168 | "os": [
169 | "linux"
170 | ],
171 | "engines": {
172 | "node": ">=12"
173 | }
174 | },
175 | "node_modules/@esbuild/linux-arm64": {
176 | "version": "0.21.5",
177 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
178 | "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
179 | "cpu": [
180 | "arm64"
181 | ],
182 | "dev": true,
183 | "optional": true,
184 | "os": [
185 | "linux"
186 | ],
187 | "engines": {
188 | "node": ">=12"
189 | }
190 | },
191 | "node_modules/@esbuild/linux-ia32": {
192 | "version": "0.21.5",
193 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
194 | "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
195 | "cpu": [
196 | "ia32"
197 | ],
198 | "dev": true,
199 | "optional": true,
200 | "os": [
201 | "linux"
202 | ],
203 | "engines": {
204 | "node": ">=12"
205 | }
206 | },
207 | "node_modules/@esbuild/linux-loong64": {
208 | "version": "0.21.5",
209 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
210 | "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
211 | "cpu": [
212 | "loong64"
213 | ],
214 | "dev": true,
215 | "optional": true,
216 | "os": [
217 | "linux"
218 | ],
219 | "engines": {
220 | "node": ">=12"
221 | }
222 | },
223 | "node_modules/@esbuild/linux-mips64el": {
224 | "version": "0.21.5",
225 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
226 | "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
227 | "cpu": [
228 | "mips64el"
229 | ],
230 | "dev": true,
231 | "optional": true,
232 | "os": [
233 | "linux"
234 | ],
235 | "engines": {
236 | "node": ">=12"
237 | }
238 | },
239 | "node_modules/@esbuild/linux-ppc64": {
240 | "version": "0.21.5",
241 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
242 | "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
243 | "cpu": [
244 | "ppc64"
245 | ],
246 | "dev": true,
247 | "optional": true,
248 | "os": [
249 | "linux"
250 | ],
251 | "engines": {
252 | "node": ">=12"
253 | }
254 | },
255 | "node_modules/@esbuild/linux-riscv64": {
256 | "version": "0.21.5",
257 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
258 | "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
259 | "cpu": [
260 | "riscv64"
261 | ],
262 | "dev": true,
263 | "optional": true,
264 | "os": [
265 | "linux"
266 | ],
267 | "engines": {
268 | "node": ">=12"
269 | }
270 | },
271 | "node_modules/@esbuild/linux-s390x": {
272 | "version": "0.21.5",
273 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
274 | "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
275 | "cpu": [
276 | "s390x"
277 | ],
278 | "dev": true,
279 | "optional": true,
280 | "os": [
281 | "linux"
282 | ],
283 | "engines": {
284 | "node": ">=12"
285 | }
286 | },
287 | "node_modules/@esbuild/linux-x64": {
288 | "version": "0.21.5",
289 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
290 | "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
291 | "cpu": [
292 | "x64"
293 | ],
294 | "dev": true,
295 | "optional": true,
296 | "os": [
297 | "linux"
298 | ],
299 | "engines": {
300 | "node": ">=12"
301 | }
302 | },
303 | "node_modules/@esbuild/netbsd-x64": {
304 | "version": "0.21.5",
305 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
306 | "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
307 | "cpu": [
308 | "x64"
309 | ],
310 | "dev": true,
311 | "optional": true,
312 | "os": [
313 | "netbsd"
314 | ],
315 | "engines": {
316 | "node": ">=12"
317 | }
318 | },
319 | "node_modules/@esbuild/openbsd-x64": {
320 | "version": "0.21.5",
321 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
322 | "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
323 | "cpu": [
324 | "x64"
325 | ],
326 | "dev": true,
327 | "optional": true,
328 | "os": [
329 | "openbsd"
330 | ],
331 | "engines": {
332 | "node": ">=12"
333 | }
334 | },
335 | "node_modules/@esbuild/sunos-x64": {
336 | "version": "0.21.5",
337 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
338 | "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
339 | "cpu": [
340 | "x64"
341 | ],
342 | "dev": true,
343 | "optional": true,
344 | "os": [
345 | "sunos"
346 | ],
347 | "engines": {
348 | "node": ">=12"
349 | }
350 | },
351 | "node_modules/@esbuild/win32-arm64": {
352 | "version": "0.21.5",
353 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
354 | "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
355 | "cpu": [
356 | "arm64"
357 | ],
358 | "dev": true,
359 | "optional": true,
360 | "os": [
361 | "win32"
362 | ],
363 | "engines": {
364 | "node": ">=12"
365 | }
366 | },
367 | "node_modules/@esbuild/win32-ia32": {
368 | "version": "0.21.5",
369 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
370 | "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
371 | "cpu": [
372 | "ia32"
373 | ],
374 | "dev": true,
375 | "optional": true,
376 | "os": [
377 | "win32"
378 | ],
379 | "engines": {
380 | "node": ">=12"
381 | }
382 | },
383 | "node_modules/@esbuild/win32-x64": {
384 | "version": "0.21.5",
385 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
386 | "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
387 | "cpu": [
388 | "x64"
389 | ],
390 | "dev": true,
391 | "optional": true,
392 | "os": [
393 | "win32"
394 | ],
395 | "engines": {
396 | "node": ">=12"
397 | }
398 | },
399 | "node_modules/@eslint-community/eslint-utils": {
400 | "version": "4.4.0",
401 | "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
402 | "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
403 | "dev": true,
404 | "dependencies": {
405 | "eslint-visitor-keys": "^3.3.0"
406 | },
407 | "engines": {
408 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
409 | },
410 | "peerDependencies": {
411 | "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
412 | }
413 | },
414 | "node_modules/@eslint-community/regexpp": {
415 | "version": "4.10.1",
416 | "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz",
417 | "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==",
418 | "dev": true,
419 | "engines": {
420 | "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
421 | }
422 | },
423 | "node_modules/@eslint/eslintrc": {
424 | "version": "2.1.4",
425 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
426 | "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
427 | "dev": true,
428 | "dependencies": {
429 | "ajv": "^6.12.4",
430 | "debug": "^4.3.2",
431 | "espree": "^9.6.0",
432 | "globals": "^13.19.0",
433 | "ignore": "^5.2.0",
434 | "import-fresh": "^3.2.1",
435 | "js-yaml": "^4.1.0",
436 | "minimatch": "^3.1.2",
437 | "strip-json-comments": "^3.1.1"
438 | },
439 | "engines": {
440 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
441 | },
442 | "funding": {
443 | "url": "https://opencollective.com/eslint"
444 | }
445 | },
446 | "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
447 | "version": "1.1.11",
448 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
449 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
450 | "dev": true,
451 | "dependencies": {
452 | "balanced-match": "^1.0.0",
453 | "concat-map": "0.0.1"
454 | }
455 | },
456 | "node_modules/@eslint/eslintrc/node_modules/minimatch": {
457 | "version": "3.1.2",
458 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
459 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
460 | "dev": true,
461 | "dependencies": {
462 | "brace-expansion": "^1.1.7"
463 | },
464 | "engines": {
465 | "node": "*"
466 | }
467 | },
468 | "node_modules/@eslint/js": {
469 | "version": "8.57.0",
470 | "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
471 | "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
472 | "dev": true,
473 | "engines": {
474 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
475 | }
476 | },
477 | "node_modules/@humanwhocodes/config-array": {
478 | "version": "0.11.14",
479 | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
480 | "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
481 | "deprecated": "Use @eslint/config-array instead",
482 | "dev": true,
483 | "dependencies": {
484 | "@humanwhocodes/object-schema": "^2.0.2",
485 | "debug": "^4.3.1",
486 | "minimatch": "^3.0.5"
487 | },
488 | "engines": {
489 | "node": ">=10.10.0"
490 | }
491 | },
492 | "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
493 | "version": "1.1.11",
494 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
495 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
496 | "dev": true,
497 | "dependencies": {
498 | "balanced-match": "^1.0.0",
499 | "concat-map": "0.0.1"
500 | }
501 | },
502 | "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
503 | "version": "3.1.2",
504 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
505 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
506 | "dev": true,
507 | "dependencies": {
508 | "brace-expansion": "^1.1.7"
509 | },
510 | "engines": {
511 | "node": "*"
512 | }
513 | },
514 | "node_modules/@humanwhocodes/module-importer": {
515 | "version": "1.0.1",
516 | "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
517 | "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
518 | "dev": true,
519 | "engines": {
520 | "node": ">=12.22"
521 | },
522 | "funding": {
523 | "type": "github",
524 | "url": "https://github.com/sponsors/nzakas"
525 | }
526 | },
527 | "node_modules/@humanwhocodes/object-schema": {
528 | "version": "2.0.3",
529 | "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
530 | "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
531 | "deprecated": "Use @eslint/object-schema instead",
532 | "dev": true
533 | },
534 | "node_modules/@nodelib/fs.scandir": {
535 | "version": "2.1.5",
536 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
537 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
538 | "dev": true,
539 | "dependencies": {
540 | "@nodelib/fs.stat": "2.0.5",
541 | "run-parallel": "^1.1.9"
542 | },
543 | "engines": {
544 | "node": ">= 8"
545 | }
546 | },
547 | "node_modules/@nodelib/fs.stat": {
548 | "version": "2.0.5",
549 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
550 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
551 | "dev": true,
552 | "engines": {
553 | "node": ">= 8"
554 | }
555 | },
556 | "node_modules/@nodelib/fs.walk": {
557 | "version": "1.2.8",
558 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
559 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
560 | "dev": true,
561 | "dependencies": {
562 | "@nodelib/fs.scandir": "2.1.5",
563 | "fastq": "^1.6.0"
564 | },
565 | "engines": {
566 | "node": ">= 8"
567 | }
568 | },
569 | "node_modules/@rollup/rollup-android-arm-eabi": {
570 | "version": "4.18.0",
571 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
572 | "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==",
573 | "cpu": [
574 | "arm"
575 | ],
576 | "dev": true,
577 | "optional": true,
578 | "os": [
579 | "android"
580 | ]
581 | },
582 | "node_modules/@rollup/rollup-android-arm64": {
583 | "version": "4.18.0",
584 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz",
585 | "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==",
586 | "cpu": [
587 | "arm64"
588 | ],
589 | "dev": true,
590 | "optional": true,
591 | "os": [
592 | "android"
593 | ]
594 | },
595 | "node_modules/@rollup/rollup-darwin-arm64": {
596 | "version": "4.18.0",
597 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz",
598 | "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==",
599 | "cpu": [
600 | "arm64"
601 | ],
602 | "dev": true,
603 | "optional": true,
604 | "os": [
605 | "darwin"
606 | ]
607 | },
608 | "node_modules/@rollup/rollup-darwin-x64": {
609 | "version": "4.18.0",
610 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz",
611 | "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==",
612 | "cpu": [
613 | "x64"
614 | ],
615 | "dev": true,
616 | "optional": true,
617 | "os": [
618 | "darwin"
619 | ]
620 | },
621 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
622 | "version": "4.18.0",
623 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz",
624 | "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==",
625 | "cpu": [
626 | "arm"
627 | ],
628 | "dev": true,
629 | "optional": true,
630 | "os": [
631 | "linux"
632 | ]
633 | },
634 | "node_modules/@rollup/rollup-linux-arm-musleabihf": {
635 | "version": "4.18.0",
636 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz",
637 | "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==",
638 | "cpu": [
639 | "arm"
640 | ],
641 | "dev": true,
642 | "optional": true,
643 | "os": [
644 | "linux"
645 | ]
646 | },
647 | "node_modules/@rollup/rollup-linux-arm64-gnu": {
648 | "version": "4.18.0",
649 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz",
650 | "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==",
651 | "cpu": [
652 | "arm64"
653 | ],
654 | "dev": true,
655 | "optional": true,
656 | "os": [
657 | "linux"
658 | ]
659 | },
660 | "node_modules/@rollup/rollup-linux-arm64-musl": {
661 | "version": "4.18.0",
662 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz",
663 | "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==",
664 | "cpu": [
665 | "arm64"
666 | ],
667 | "dev": true,
668 | "optional": true,
669 | "os": [
670 | "linux"
671 | ]
672 | },
673 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
674 | "version": "4.18.0",
675 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz",
676 | "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==",
677 | "cpu": [
678 | "ppc64"
679 | ],
680 | "dev": true,
681 | "optional": true,
682 | "os": [
683 | "linux"
684 | ]
685 | },
686 | "node_modules/@rollup/rollup-linux-riscv64-gnu": {
687 | "version": "4.18.0",
688 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz",
689 | "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==",
690 | "cpu": [
691 | "riscv64"
692 | ],
693 | "dev": true,
694 | "optional": true,
695 | "os": [
696 | "linux"
697 | ]
698 | },
699 | "node_modules/@rollup/rollup-linux-s390x-gnu": {
700 | "version": "4.18.0",
701 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz",
702 | "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==",
703 | "cpu": [
704 | "s390x"
705 | ],
706 | "dev": true,
707 | "optional": true,
708 | "os": [
709 | "linux"
710 | ]
711 | },
712 | "node_modules/@rollup/rollup-linux-x64-gnu": {
713 | "version": "4.18.0",
714 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
715 | "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
716 | "cpu": [
717 | "x64"
718 | ],
719 | "dev": true,
720 | "optional": true,
721 | "os": [
722 | "linux"
723 | ]
724 | },
725 | "node_modules/@rollup/rollup-linux-x64-musl": {
726 | "version": "4.18.0",
727 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz",
728 | "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==",
729 | "cpu": [
730 | "x64"
731 | ],
732 | "dev": true,
733 | "optional": true,
734 | "os": [
735 | "linux"
736 | ]
737 | },
738 | "node_modules/@rollup/rollup-win32-arm64-msvc": {
739 | "version": "4.18.0",
740 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz",
741 | "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==",
742 | "cpu": [
743 | "arm64"
744 | ],
745 | "dev": true,
746 | "optional": true,
747 | "os": [
748 | "win32"
749 | ]
750 | },
751 | "node_modules/@rollup/rollup-win32-ia32-msvc": {
752 | "version": "4.18.0",
753 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz",
754 | "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==",
755 | "cpu": [
756 | "ia32"
757 | ],
758 | "dev": true,
759 | "optional": true,
760 | "os": [
761 | "win32"
762 | ]
763 | },
764 | "node_modules/@rollup/rollup-win32-x64-msvc": {
765 | "version": "4.18.0",
766 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz",
767 | "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==",
768 | "cpu": [
769 | "x64"
770 | ],
771 | "dev": true,
772 | "optional": true,
773 | "os": [
774 | "win32"
775 | ]
776 | },
777 | "node_modules/@swc/core": {
778 | "version": "1.6.1",
779 | "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.1.tgz",
780 | "integrity": "sha512-Yz5uj5hNZpS5brLtBvKY0L4s2tBAbQ4TjmW8xF1EC3YLFxQRrUjMP49Zm1kp/KYyYvTkSaG48Ffj2YWLu9nChw==",
781 | "dev": true,
782 | "hasInstallScript": true,
783 | "dependencies": {
784 | "@swc/counter": "^0.1.3",
785 | "@swc/types": "^0.1.8"
786 | },
787 | "engines": {
788 | "node": ">=10"
789 | },
790 | "funding": {
791 | "type": "opencollective",
792 | "url": "https://opencollective.com/swc"
793 | },
794 | "optionalDependencies": {
795 | "@swc/core-darwin-arm64": "1.6.1",
796 | "@swc/core-darwin-x64": "1.6.1",
797 | "@swc/core-linux-arm-gnueabihf": "1.6.1",
798 | "@swc/core-linux-arm64-gnu": "1.6.1",
799 | "@swc/core-linux-arm64-musl": "1.6.1",
800 | "@swc/core-linux-x64-gnu": "1.6.1",
801 | "@swc/core-linux-x64-musl": "1.6.1",
802 | "@swc/core-win32-arm64-msvc": "1.6.1",
803 | "@swc/core-win32-ia32-msvc": "1.6.1",
804 | "@swc/core-win32-x64-msvc": "1.6.1"
805 | },
806 | "peerDependencies": {
807 | "@swc/helpers": "*"
808 | },
809 | "peerDependenciesMeta": {
810 | "@swc/helpers": {
811 | "optional": true
812 | }
813 | }
814 | },
815 | "node_modules/@swc/core-darwin-arm64": {
816 | "version": "1.6.1",
817 | "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.1.tgz",
818 | "integrity": "sha512-u6GdwOXsOEdNAdSI6nWq6G2BQw5HiSNIZVcBaH1iSvBnxZvWbnIKyDiZKaYnDwTLHLzig2GuUjjE2NaCJPy4jg==",
819 | "cpu": [
820 | "arm64"
821 | ],
822 | "dev": true,
823 | "optional": true,
824 | "os": [
825 | "darwin"
826 | ],
827 | "engines": {
828 | "node": ">=10"
829 | }
830 | },
831 | "node_modules/@swc/core-darwin-x64": {
832 | "version": "1.6.1",
833 | "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.1.tgz",
834 | "integrity": "sha512-/tXwQibkDNLVbAtr7PUQI0iQjoB708fjhDDDfJ6WILSBVZ3+qs/LHjJ7jHwumEYxVq1XA7Fv2Q7SE/ZSQoWHcQ==",
835 | "cpu": [
836 | "x64"
837 | ],
838 | "dev": true,
839 | "optional": true,
840 | "os": [
841 | "darwin"
842 | ],
843 | "engines": {
844 | "node": ">=10"
845 | }
846 | },
847 | "node_modules/@swc/core-linux-arm-gnueabihf": {
848 | "version": "1.6.1",
849 | "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.1.tgz",
850 | "integrity": "sha512-aDgipxhJTms8iH78emHVutFR2c16LNhO+NTRCdYi+X4PyIn58/DyYTH6VDZ0AeEcS5f132ZFldU5AEgExwihXA==",
851 | "cpu": [
852 | "arm"
853 | ],
854 | "dev": true,
855 | "optional": true,
856 | "os": [
857 | "linux"
858 | ],
859 | "engines": {
860 | "node": ">=10"
861 | }
862 | },
863 | "node_modules/@swc/core-linux-arm64-gnu": {
864 | "version": "1.6.1",
865 | "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.1.tgz",
866 | "integrity": "sha512-XkJ+eO4zUKG5g458RyhmKPyBGxI0FwfWFgpfIj5eDybxYJ6s4HBT5MoxyBLorB5kMlZ0XoY/usUMobPVY3nL0g==",
867 | "cpu": [
868 | "arm64"
869 | ],
870 | "dev": true,
871 | "optional": true,
872 | "os": [
873 | "linux"
874 | ],
875 | "engines": {
876 | "node": ">=10"
877 | }
878 | },
879 | "node_modules/@swc/core-linux-arm64-musl": {
880 | "version": "1.6.1",
881 | "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.1.tgz",
882 | "integrity": "sha512-dr6YbLBg/SsNxs1hDqJhxdcrS8dGMlOXJwXIrUvACiA8jAd6S5BxYCaqsCefLYXtaOmu0bbx1FB/evfodqB70Q==",
883 | "cpu": [
884 | "arm64"
885 | ],
886 | "dev": true,
887 | "optional": true,
888 | "os": [
889 | "linux"
890 | ],
891 | "engines": {
892 | "node": ">=10"
893 | }
894 | },
895 | "node_modules/@swc/core-linux-x64-gnu": {
896 | "version": "1.6.1",
897 | "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.1.tgz",
898 | "integrity": "sha512-A0b/3V+yFy4LXh3O9umIE7LXPC7NBWdjl6AQYqymSMcMu0EOb1/iygA6s6uWhz9y3e172Hpb9b/CGsuD8Px/bg==",
899 | "cpu": [
900 | "x64"
901 | ],
902 | "dev": true,
903 | "optional": true,
904 | "os": [
905 | "linux"
906 | ],
907 | "engines": {
908 | "node": ">=10"
909 | }
910 | },
911 | "node_modules/@swc/core-linux-x64-musl": {
912 | "version": "1.6.1",
913 | "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.1.tgz",
914 | "integrity": "sha512-5dJjlzZXhC87nZZZWbpiDP8kBIO0ibis893F/rtPIQBI5poH+iJuA32EU3wN4/WFHeK4et8z6SGSVghPtWyk4g==",
915 | "cpu": [
916 | "x64"
917 | ],
918 | "dev": true,
919 | "optional": true,
920 | "os": [
921 | "linux"
922 | ],
923 | "engines": {
924 | "node": ">=10"
925 | }
926 | },
927 | "node_modules/@swc/core-win32-arm64-msvc": {
928 | "version": "1.6.1",
929 | "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.1.tgz",
930 | "integrity": "sha512-HBi1ZlwvfcUibLtT3g/lP57FaDPC799AD6InolB2KSgkqyBbZJ9wAXM8/CcH67GLIP0tZ7FqblrJTzGXxetTJQ==",
931 | "cpu": [
932 | "arm64"
933 | ],
934 | "dev": true,
935 | "optional": true,
936 | "os": [
937 | "win32"
938 | ],
939 | "engines": {
940 | "node": ">=10"
941 | }
942 | },
943 | "node_modules/@swc/core-win32-ia32-msvc": {
944 | "version": "1.6.1",
945 | "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.1.tgz",
946 | "integrity": "sha512-AKqHohlWERclexar5y6ux4sQ8yaMejEXNxeKXm7xPhXrp13/1p4/I3E5bPVX/jMnvpm4HpcKSP0ee2WsqmhhPw==",
947 | "cpu": [
948 | "ia32"
949 | ],
950 | "dev": true,
951 | "optional": true,
952 | "os": [
953 | "win32"
954 | ],
955 | "engines": {
956 | "node": ">=10"
957 | }
958 | },
959 | "node_modules/@swc/core-win32-x64-msvc": {
960 | "version": "1.6.1",
961 | "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.1.tgz",
962 | "integrity": "sha512-0dLdTLd+ONve8kgC5T6VQ2Y5G+OZ7y0ujjapnK66wpvCBM6BKYGdT/OKhZKZydrC5gUKaxFN6Y5oOt9JOFUrOQ==",
963 | "cpu": [
964 | "x64"
965 | ],
966 | "dev": true,
967 | "optional": true,
968 | "os": [
969 | "win32"
970 | ],
971 | "engines": {
972 | "node": ">=10"
973 | }
974 | },
975 | "node_modules/@swc/counter": {
976 | "version": "0.1.3",
977 | "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
978 | "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
979 | "dev": true
980 | },
981 | "node_modules/@swc/helpers": {
982 | "version": "0.5.11",
983 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz",
984 | "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==",
985 | "dependencies": {
986 | "tslib": "^2.4.0"
987 | }
988 | },
989 | "node_modules/@swc/types": {
990 | "version": "0.1.8",
991 | "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.8.tgz",
992 | "integrity": "sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==",
993 | "dev": true,
994 | "dependencies": {
995 | "@swc/counter": "^0.1.3"
996 | }
997 | },
998 | "node_modules/@types/estree": {
999 | "version": "1.0.5",
1000 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
1001 | "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
1002 | "dev": true
1003 | },
1004 | "node_modules/@types/prop-types": {
1005 | "version": "15.7.12",
1006 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
1007 | "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
1008 | "dev": true
1009 | },
1010 | "node_modules/@types/react": {
1011 | "version": "18.3.3",
1012 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
1013 | "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
1014 | "dev": true,
1015 | "dependencies": {
1016 | "@types/prop-types": "*",
1017 | "csstype": "^3.0.2"
1018 | }
1019 | },
1020 | "node_modules/@types/react-dom": {
1021 | "version": "18.3.0",
1022 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
1023 | "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
1024 | "dev": true,
1025 | "dependencies": {
1026 | "@types/react": "*"
1027 | }
1028 | },
1029 | "node_modules/@typescript-eslint/eslint-plugin": {
1030 | "version": "7.13.0",
1031 | "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.0.tgz",
1032 | "integrity": "sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==",
1033 | "dev": true,
1034 | "dependencies": {
1035 | "@eslint-community/regexpp": "^4.10.0",
1036 | "@typescript-eslint/scope-manager": "7.13.0",
1037 | "@typescript-eslint/type-utils": "7.13.0",
1038 | "@typescript-eslint/utils": "7.13.0",
1039 | "@typescript-eslint/visitor-keys": "7.13.0",
1040 | "graphemer": "^1.4.0",
1041 | "ignore": "^5.3.1",
1042 | "natural-compare": "^1.4.0",
1043 | "ts-api-utils": "^1.3.0"
1044 | },
1045 | "engines": {
1046 | "node": "^18.18.0 || >=20.0.0"
1047 | },
1048 | "funding": {
1049 | "type": "opencollective",
1050 | "url": "https://opencollective.com/typescript-eslint"
1051 | },
1052 | "peerDependencies": {
1053 | "@typescript-eslint/parser": "^7.0.0",
1054 | "eslint": "^8.56.0"
1055 | },
1056 | "peerDependenciesMeta": {
1057 | "typescript": {
1058 | "optional": true
1059 | }
1060 | }
1061 | },
1062 | "node_modules/@typescript-eslint/parser": {
1063 | "version": "7.13.0",
1064 | "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.0.tgz",
1065 | "integrity": "sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA==",
1066 | "dev": true,
1067 | "dependencies": {
1068 | "@typescript-eslint/scope-manager": "7.13.0",
1069 | "@typescript-eslint/types": "7.13.0",
1070 | "@typescript-eslint/typescript-estree": "7.13.0",
1071 | "@typescript-eslint/visitor-keys": "7.13.0",
1072 | "debug": "^4.3.4"
1073 | },
1074 | "engines": {
1075 | "node": "^18.18.0 || >=20.0.0"
1076 | },
1077 | "funding": {
1078 | "type": "opencollective",
1079 | "url": "https://opencollective.com/typescript-eslint"
1080 | },
1081 | "peerDependencies": {
1082 | "eslint": "^8.56.0"
1083 | },
1084 | "peerDependenciesMeta": {
1085 | "typescript": {
1086 | "optional": true
1087 | }
1088 | }
1089 | },
1090 | "node_modules/@typescript-eslint/scope-manager": {
1091 | "version": "7.13.0",
1092 | "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz",
1093 | "integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==",
1094 | "dev": true,
1095 | "dependencies": {
1096 | "@typescript-eslint/types": "7.13.0",
1097 | "@typescript-eslint/visitor-keys": "7.13.0"
1098 | },
1099 | "engines": {
1100 | "node": "^18.18.0 || >=20.0.0"
1101 | },
1102 | "funding": {
1103 | "type": "opencollective",
1104 | "url": "https://opencollective.com/typescript-eslint"
1105 | }
1106 | },
1107 | "node_modules/@typescript-eslint/type-utils": {
1108 | "version": "7.13.0",
1109 | "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.0.tgz",
1110 | "integrity": "sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==",
1111 | "dev": true,
1112 | "dependencies": {
1113 | "@typescript-eslint/typescript-estree": "7.13.0",
1114 | "@typescript-eslint/utils": "7.13.0",
1115 | "debug": "^4.3.4",
1116 | "ts-api-utils": "^1.3.0"
1117 | },
1118 | "engines": {
1119 | "node": "^18.18.0 || >=20.0.0"
1120 | },
1121 | "funding": {
1122 | "type": "opencollective",
1123 | "url": "https://opencollective.com/typescript-eslint"
1124 | },
1125 | "peerDependencies": {
1126 | "eslint": "^8.56.0"
1127 | },
1128 | "peerDependenciesMeta": {
1129 | "typescript": {
1130 | "optional": true
1131 | }
1132 | }
1133 | },
1134 | "node_modules/@typescript-eslint/types": {
1135 | "version": "7.13.0",
1136 | "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz",
1137 | "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==",
1138 | "dev": true,
1139 | "engines": {
1140 | "node": "^18.18.0 || >=20.0.0"
1141 | },
1142 | "funding": {
1143 | "type": "opencollective",
1144 | "url": "https://opencollective.com/typescript-eslint"
1145 | }
1146 | },
1147 | "node_modules/@typescript-eslint/typescript-estree": {
1148 | "version": "7.13.0",
1149 | "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz",
1150 | "integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==",
1151 | "dev": true,
1152 | "dependencies": {
1153 | "@typescript-eslint/types": "7.13.0",
1154 | "@typescript-eslint/visitor-keys": "7.13.0",
1155 | "debug": "^4.3.4",
1156 | "globby": "^11.1.0",
1157 | "is-glob": "^4.0.3",
1158 | "minimatch": "^9.0.4",
1159 | "semver": "^7.6.0",
1160 | "ts-api-utils": "^1.3.0"
1161 | },
1162 | "engines": {
1163 | "node": "^18.18.0 || >=20.0.0"
1164 | },
1165 | "funding": {
1166 | "type": "opencollective",
1167 | "url": "https://opencollective.com/typescript-eslint"
1168 | },
1169 | "peerDependenciesMeta": {
1170 | "typescript": {
1171 | "optional": true
1172 | }
1173 | }
1174 | },
1175 | "node_modules/@typescript-eslint/utils": {
1176 | "version": "7.13.0",
1177 | "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.0.tgz",
1178 | "integrity": "sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==",
1179 | "dev": true,
1180 | "dependencies": {
1181 | "@eslint-community/eslint-utils": "^4.4.0",
1182 | "@typescript-eslint/scope-manager": "7.13.0",
1183 | "@typescript-eslint/types": "7.13.0",
1184 | "@typescript-eslint/typescript-estree": "7.13.0"
1185 | },
1186 | "engines": {
1187 | "node": "^18.18.0 || >=20.0.0"
1188 | },
1189 | "funding": {
1190 | "type": "opencollective",
1191 | "url": "https://opencollective.com/typescript-eslint"
1192 | },
1193 | "peerDependencies": {
1194 | "eslint": "^8.56.0"
1195 | }
1196 | },
1197 | "node_modules/@typescript-eslint/visitor-keys": {
1198 | "version": "7.13.0",
1199 | "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz",
1200 | "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==",
1201 | "dev": true,
1202 | "dependencies": {
1203 | "@typescript-eslint/types": "7.13.0",
1204 | "eslint-visitor-keys": "^3.4.3"
1205 | },
1206 | "engines": {
1207 | "node": "^18.18.0 || >=20.0.0"
1208 | },
1209 | "funding": {
1210 | "type": "opencollective",
1211 | "url": "https://opencollective.com/typescript-eslint"
1212 | }
1213 | },
1214 | "node_modules/@ungap/structured-clone": {
1215 | "version": "1.2.0",
1216 | "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
1217 | "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
1218 | "dev": true
1219 | },
1220 | "node_modules/@vitejs/plugin-react-swc": {
1221 | "version": "3.7.0",
1222 | "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz",
1223 | "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==",
1224 | "dev": true,
1225 | "dependencies": {
1226 | "@swc/core": "^1.5.7"
1227 | },
1228 | "peerDependencies": {
1229 | "vite": "^4 || ^5"
1230 | }
1231 | },
1232 | "node_modules/acorn": {
1233 | "version": "8.12.0",
1234 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
1235 | "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
1236 | "dev": true,
1237 | "bin": {
1238 | "acorn": "bin/acorn"
1239 | },
1240 | "engines": {
1241 | "node": ">=0.4.0"
1242 | }
1243 | },
1244 | "node_modules/acorn-jsx": {
1245 | "version": "5.3.2",
1246 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
1247 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
1248 | "dev": true,
1249 | "peerDependencies": {
1250 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1251 | }
1252 | },
1253 | "node_modules/ajv": {
1254 | "version": "6.12.6",
1255 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
1256 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
1257 | "dev": true,
1258 | "dependencies": {
1259 | "fast-deep-equal": "^3.1.1",
1260 | "fast-json-stable-stringify": "^2.0.0",
1261 | "json-schema-traverse": "^0.4.1",
1262 | "uri-js": "^4.2.2"
1263 | },
1264 | "funding": {
1265 | "type": "github",
1266 | "url": "https://github.com/sponsors/epoberezkin"
1267 | }
1268 | },
1269 | "node_modules/ansi-regex": {
1270 | "version": "5.0.1",
1271 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1272 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1273 | "dev": true,
1274 | "engines": {
1275 | "node": ">=8"
1276 | }
1277 | },
1278 | "node_modules/ansi-styles": {
1279 | "version": "4.3.0",
1280 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
1281 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
1282 | "dev": true,
1283 | "dependencies": {
1284 | "color-convert": "^2.0.1"
1285 | },
1286 | "engines": {
1287 | "node": ">=8"
1288 | },
1289 | "funding": {
1290 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1291 | }
1292 | },
1293 | "node_modules/argparse": {
1294 | "version": "2.0.1",
1295 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
1296 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
1297 | "dev": true
1298 | },
1299 | "node_modules/array-union": {
1300 | "version": "2.1.0",
1301 | "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
1302 | "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
1303 | "dev": true,
1304 | "engines": {
1305 | "node": ">=8"
1306 | }
1307 | },
1308 | "node_modules/balanced-match": {
1309 | "version": "1.0.2",
1310 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
1311 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
1312 | "dev": true
1313 | },
1314 | "node_modules/brace-expansion": {
1315 | "version": "2.0.1",
1316 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
1317 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
1318 | "dev": true,
1319 | "dependencies": {
1320 | "balanced-match": "^1.0.0"
1321 | }
1322 | },
1323 | "node_modules/braces": {
1324 | "version": "3.0.3",
1325 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
1326 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
1327 | "dev": true,
1328 | "dependencies": {
1329 | "fill-range": "^7.1.1"
1330 | },
1331 | "engines": {
1332 | "node": ">=8"
1333 | }
1334 | },
1335 | "node_modules/callsites": {
1336 | "version": "3.1.0",
1337 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
1338 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
1339 | "dev": true,
1340 | "engines": {
1341 | "node": ">=6"
1342 | }
1343 | },
1344 | "node_modules/chalk": {
1345 | "version": "4.1.2",
1346 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
1347 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
1348 | "dev": true,
1349 | "dependencies": {
1350 | "ansi-styles": "^4.1.0",
1351 | "supports-color": "^7.1.0"
1352 | },
1353 | "engines": {
1354 | "node": ">=10"
1355 | },
1356 | "funding": {
1357 | "url": "https://github.com/chalk/chalk?sponsor=1"
1358 | }
1359 | },
1360 | "node_modules/client-only": {
1361 | "version": "0.0.1",
1362 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
1363 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
1364 | },
1365 | "node_modules/clsx": {
1366 | "version": "2.1.1",
1367 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1368 | "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1369 | "engines": {
1370 | "node": ">=6"
1371 | }
1372 | },
1373 | "node_modules/color-convert": {
1374 | "version": "2.0.1",
1375 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1376 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1377 | "dev": true,
1378 | "dependencies": {
1379 | "color-name": "~1.1.4"
1380 | },
1381 | "engines": {
1382 | "node": ">=7.0.0"
1383 | }
1384 | },
1385 | "node_modules/color-name": {
1386 | "version": "1.1.4",
1387 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1388 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1389 | "dev": true
1390 | },
1391 | "node_modules/concat-map": {
1392 | "version": "0.0.1",
1393 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
1394 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
1395 | "dev": true
1396 | },
1397 | "node_modules/cross-spawn": {
1398 | "version": "7.0.3",
1399 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
1400 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
1401 | "dev": true,
1402 | "dependencies": {
1403 | "path-key": "^3.1.0",
1404 | "shebang-command": "^2.0.0",
1405 | "which": "^2.0.1"
1406 | },
1407 | "engines": {
1408 | "node": ">= 8"
1409 | }
1410 | },
1411 | "node_modules/csstype": {
1412 | "version": "3.1.3",
1413 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
1414 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
1415 | "dev": true
1416 | },
1417 | "node_modules/debug": {
1418 | "version": "4.3.5",
1419 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
1420 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
1421 | "dev": true,
1422 | "dependencies": {
1423 | "ms": "2.1.2"
1424 | },
1425 | "engines": {
1426 | "node": ">=6.0"
1427 | },
1428 | "peerDependenciesMeta": {
1429 | "supports-color": {
1430 | "optional": true
1431 | }
1432 | }
1433 | },
1434 | "node_modules/deep-is": {
1435 | "version": "0.1.4",
1436 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
1437 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
1438 | "dev": true
1439 | },
1440 | "node_modules/dir-glob": {
1441 | "version": "3.0.1",
1442 | "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
1443 | "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
1444 | "dev": true,
1445 | "dependencies": {
1446 | "path-type": "^4.0.0"
1447 | },
1448 | "engines": {
1449 | "node": ">=8"
1450 | }
1451 | },
1452 | "node_modules/doctrine": {
1453 | "version": "3.0.0",
1454 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
1455 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
1456 | "dev": true,
1457 | "dependencies": {
1458 | "esutils": "^2.0.2"
1459 | },
1460 | "engines": {
1461 | "node": ">=6.0.0"
1462 | }
1463 | },
1464 | "node_modules/esbuild": {
1465 | "version": "0.21.5",
1466 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1467 | "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1468 | "dev": true,
1469 | "hasInstallScript": true,
1470 | "bin": {
1471 | "esbuild": "bin/esbuild"
1472 | },
1473 | "engines": {
1474 | "node": ">=12"
1475 | },
1476 | "optionalDependencies": {
1477 | "@esbuild/aix-ppc64": "0.21.5",
1478 | "@esbuild/android-arm": "0.21.5",
1479 | "@esbuild/android-arm64": "0.21.5",
1480 | "@esbuild/android-x64": "0.21.5",
1481 | "@esbuild/darwin-arm64": "0.21.5",
1482 | "@esbuild/darwin-x64": "0.21.5",
1483 | "@esbuild/freebsd-arm64": "0.21.5",
1484 | "@esbuild/freebsd-x64": "0.21.5",
1485 | "@esbuild/linux-arm": "0.21.5",
1486 | "@esbuild/linux-arm64": "0.21.5",
1487 | "@esbuild/linux-ia32": "0.21.5",
1488 | "@esbuild/linux-loong64": "0.21.5",
1489 | "@esbuild/linux-mips64el": "0.21.5",
1490 | "@esbuild/linux-ppc64": "0.21.5",
1491 | "@esbuild/linux-riscv64": "0.21.5",
1492 | "@esbuild/linux-s390x": "0.21.5",
1493 | "@esbuild/linux-x64": "0.21.5",
1494 | "@esbuild/netbsd-x64": "0.21.5",
1495 | "@esbuild/openbsd-x64": "0.21.5",
1496 | "@esbuild/sunos-x64": "0.21.5",
1497 | "@esbuild/win32-arm64": "0.21.5",
1498 | "@esbuild/win32-ia32": "0.21.5",
1499 | "@esbuild/win32-x64": "0.21.5"
1500 | }
1501 | },
1502 | "node_modules/escape-string-regexp": {
1503 | "version": "4.0.0",
1504 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1505 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1506 | "dev": true,
1507 | "engines": {
1508 | "node": ">=10"
1509 | },
1510 | "funding": {
1511 | "url": "https://github.com/sponsors/sindresorhus"
1512 | }
1513 | },
1514 | "node_modules/eslint": {
1515 | "version": "8.57.0",
1516 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
1517 | "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
1518 | "dev": true,
1519 | "dependencies": {
1520 | "@eslint-community/eslint-utils": "^4.2.0",
1521 | "@eslint-community/regexpp": "^4.6.1",
1522 | "@eslint/eslintrc": "^2.1.4",
1523 | "@eslint/js": "8.57.0",
1524 | "@humanwhocodes/config-array": "^0.11.14",
1525 | "@humanwhocodes/module-importer": "^1.0.1",
1526 | "@nodelib/fs.walk": "^1.2.8",
1527 | "@ungap/structured-clone": "^1.2.0",
1528 | "ajv": "^6.12.4",
1529 | "chalk": "^4.0.0",
1530 | "cross-spawn": "^7.0.2",
1531 | "debug": "^4.3.2",
1532 | "doctrine": "^3.0.0",
1533 | "escape-string-regexp": "^4.0.0",
1534 | "eslint-scope": "^7.2.2",
1535 | "eslint-visitor-keys": "^3.4.3",
1536 | "espree": "^9.6.1",
1537 | "esquery": "^1.4.2",
1538 | "esutils": "^2.0.2",
1539 | "fast-deep-equal": "^3.1.3",
1540 | "file-entry-cache": "^6.0.1",
1541 | "find-up": "^5.0.0",
1542 | "glob-parent": "^6.0.2",
1543 | "globals": "^13.19.0",
1544 | "graphemer": "^1.4.0",
1545 | "ignore": "^5.2.0",
1546 | "imurmurhash": "^0.1.4",
1547 | "is-glob": "^4.0.0",
1548 | "is-path-inside": "^3.0.3",
1549 | "js-yaml": "^4.1.0",
1550 | "json-stable-stringify-without-jsonify": "^1.0.1",
1551 | "levn": "^0.4.1",
1552 | "lodash.merge": "^4.6.2",
1553 | "minimatch": "^3.1.2",
1554 | "natural-compare": "^1.4.0",
1555 | "optionator": "^0.9.3",
1556 | "strip-ansi": "^6.0.1",
1557 | "text-table": "^0.2.0"
1558 | },
1559 | "bin": {
1560 | "eslint": "bin/eslint.js"
1561 | },
1562 | "engines": {
1563 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1564 | },
1565 | "funding": {
1566 | "url": "https://opencollective.com/eslint"
1567 | }
1568 | },
1569 | "node_modules/eslint-plugin-react-hooks": {
1570 | "version": "4.6.2",
1571 | "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
1572 | "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
1573 | "dev": true,
1574 | "engines": {
1575 | "node": ">=10"
1576 | },
1577 | "peerDependencies": {
1578 | "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
1579 | }
1580 | },
1581 | "node_modules/eslint-plugin-react-refresh": {
1582 | "version": "0.4.7",
1583 | "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz",
1584 | "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==",
1585 | "dev": true,
1586 | "peerDependencies": {
1587 | "eslint": ">=7"
1588 | }
1589 | },
1590 | "node_modules/eslint-scope": {
1591 | "version": "7.2.2",
1592 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
1593 | "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
1594 | "dev": true,
1595 | "dependencies": {
1596 | "esrecurse": "^4.3.0",
1597 | "estraverse": "^5.2.0"
1598 | },
1599 | "engines": {
1600 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1601 | },
1602 | "funding": {
1603 | "url": "https://opencollective.com/eslint"
1604 | }
1605 | },
1606 | "node_modules/eslint-visitor-keys": {
1607 | "version": "3.4.3",
1608 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
1609 | "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
1610 | "dev": true,
1611 | "engines": {
1612 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1613 | },
1614 | "funding": {
1615 | "url": "https://opencollective.com/eslint"
1616 | }
1617 | },
1618 | "node_modules/eslint/node_modules/brace-expansion": {
1619 | "version": "1.1.11",
1620 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
1621 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
1622 | "dev": true,
1623 | "dependencies": {
1624 | "balanced-match": "^1.0.0",
1625 | "concat-map": "0.0.1"
1626 | }
1627 | },
1628 | "node_modules/eslint/node_modules/minimatch": {
1629 | "version": "3.1.2",
1630 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1631 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1632 | "dev": true,
1633 | "dependencies": {
1634 | "brace-expansion": "^1.1.7"
1635 | },
1636 | "engines": {
1637 | "node": "*"
1638 | }
1639 | },
1640 | "node_modules/espree": {
1641 | "version": "9.6.1",
1642 | "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
1643 | "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
1644 | "dev": true,
1645 | "dependencies": {
1646 | "acorn": "^8.9.0",
1647 | "acorn-jsx": "^5.3.2",
1648 | "eslint-visitor-keys": "^3.4.1"
1649 | },
1650 | "engines": {
1651 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1652 | },
1653 | "funding": {
1654 | "url": "https://opencollective.com/eslint"
1655 | }
1656 | },
1657 | "node_modules/esquery": {
1658 | "version": "1.5.0",
1659 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
1660 | "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
1661 | "dev": true,
1662 | "dependencies": {
1663 | "estraverse": "^5.1.0"
1664 | },
1665 | "engines": {
1666 | "node": ">=0.10"
1667 | }
1668 | },
1669 | "node_modules/esrecurse": {
1670 | "version": "4.3.0",
1671 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
1672 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
1673 | "dev": true,
1674 | "dependencies": {
1675 | "estraverse": "^5.2.0"
1676 | },
1677 | "engines": {
1678 | "node": ">=4.0"
1679 | }
1680 | },
1681 | "node_modules/estraverse": {
1682 | "version": "5.3.0",
1683 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
1684 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
1685 | "dev": true,
1686 | "engines": {
1687 | "node": ">=4.0"
1688 | }
1689 | },
1690 | "node_modules/esutils": {
1691 | "version": "2.0.3",
1692 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
1693 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
1694 | "dev": true,
1695 | "engines": {
1696 | "node": ">=0.10.0"
1697 | }
1698 | },
1699 | "node_modules/fast-deep-equal": {
1700 | "version": "3.1.3",
1701 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1702 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1703 | "dev": true
1704 | },
1705 | "node_modules/fast-glob": {
1706 | "version": "3.3.2",
1707 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
1708 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
1709 | "dev": true,
1710 | "dependencies": {
1711 | "@nodelib/fs.stat": "^2.0.2",
1712 | "@nodelib/fs.walk": "^1.2.3",
1713 | "glob-parent": "^5.1.2",
1714 | "merge2": "^1.3.0",
1715 | "micromatch": "^4.0.4"
1716 | },
1717 | "engines": {
1718 | "node": ">=8.6.0"
1719 | }
1720 | },
1721 | "node_modules/fast-glob/node_modules/glob-parent": {
1722 | "version": "5.1.2",
1723 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1724 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1725 | "dev": true,
1726 | "dependencies": {
1727 | "is-glob": "^4.0.1"
1728 | },
1729 | "engines": {
1730 | "node": ">= 6"
1731 | }
1732 | },
1733 | "node_modules/fast-json-stable-stringify": {
1734 | "version": "2.1.0",
1735 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
1736 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
1737 | "dev": true
1738 | },
1739 | "node_modules/fast-levenshtein": {
1740 | "version": "2.0.6",
1741 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
1742 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
1743 | "dev": true
1744 | },
1745 | "node_modules/fastq": {
1746 | "version": "1.17.1",
1747 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
1748 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
1749 | "dev": true,
1750 | "dependencies": {
1751 | "reusify": "^1.0.4"
1752 | }
1753 | },
1754 | "node_modules/file-entry-cache": {
1755 | "version": "6.0.1",
1756 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
1757 | "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
1758 | "dev": true,
1759 | "dependencies": {
1760 | "flat-cache": "^3.0.4"
1761 | },
1762 | "engines": {
1763 | "node": "^10.12.0 || >=12.0.0"
1764 | }
1765 | },
1766 | "node_modules/fill-range": {
1767 | "version": "7.1.1",
1768 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
1769 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
1770 | "dev": true,
1771 | "dependencies": {
1772 | "to-regex-range": "^5.0.1"
1773 | },
1774 | "engines": {
1775 | "node": ">=8"
1776 | }
1777 | },
1778 | "node_modules/find-up": {
1779 | "version": "5.0.0",
1780 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
1781 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
1782 | "dev": true,
1783 | "dependencies": {
1784 | "locate-path": "^6.0.0",
1785 | "path-exists": "^4.0.0"
1786 | },
1787 | "engines": {
1788 | "node": ">=10"
1789 | },
1790 | "funding": {
1791 | "url": "https://github.com/sponsors/sindresorhus"
1792 | }
1793 | },
1794 | "node_modules/flat-cache": {
1795 | "version": "3.2.0",
1796 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
1797 | "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
1798 | "dev": true,
1799 | "dependencies": {
1800 | "flatted": "^3.2.9",
1801 | "keyv": "^4.5.3",
1802 | "rimraf": "^3.0.2"
1803 | },
1804 | "engines": {
1805 | "node": "^10.12.0 || >=12.0.0"
1806 | }
1807 | },
1808 | "node_modules/flatted": {
1809 | "version": "3.3.1",
1810 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
1811 | "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
1812 | "dev": true
1813 | },
1814 | "node_modules/fs.realpath": {
1815 | "version": "1.0.0",
1816 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
1817 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
1818 | "dev": true
1819 | },
1820 | "node_modules/fsevents": {
1821 | "version": "2.3.3",
1822 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1823 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1824 | "dev": true,
1825 | "hasInstallScript": true,
1826 | "optional": true,
1827 | "os": [
1828 | "darwin"
1829 | ],
1830 | "engines": {
1831 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1832 | }
1833 | },
1834 | "node_modules/glob": {
1835 | "version": "7.2.3",
1836 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
1837 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
1838 | "deprecated": "Glob versions prior to v9 are no longer supported",
1839 | "dev": true,
1840 | "dependencies": {
1841 | "fs.realpath": "^1.0.0",
1842 | "inflight": "^1.0.4",
1843 | "inherits": "2",
1844 | "minimatch": "^3.1.1",
1845 | "once": "^1.3.0",
1846 | "path-is-absolute": "^1.0.0"
1847 | },
1848 | "engines": {
1849 | "node": "*"
1850 | },
1851 | "funding": {
1852 | "url": "https://github.com/sponsors/isaacs"
1853 | }
1854 | },
1855 | "node_modules/glob-parent": {
1856 | "version": "6.0.2",
1857 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1858 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1859 | "dev": true,
1860 | "dependencies": {
1861 | "is-glob": "^4.0.3"
1862 | },
1863 | "engines": {
1864 | "node": ">=10.13.0"
1865 | }
1866 | },
1867 | "node_modules/glob/node_modules/brace-expansion": {
1868 | "version": "1.1.11",
1869 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
1870 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
1871 | "dev": true,
1872 | "dependencies": {
1873 | "balanced-match": "^1.0.0",
1874 | "concat-map": "0.0.1"
1875 | }
1876 | },
1877 | "node_modules/glob/node_modules/minimatch": {
1878 | "version": "3.1.2",
1879 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1880 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1881 | "dev": true,
1882 | "dependencies": {
1883 | "brace-expansion": "^1.1.7"
1884 | },
1885 | "engines": {
1886 | "node": "*"
1887 | }
1888 | },
1889 | "node_modules/globals": {
1890 | "version": "13.24.0",
1891 | "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
1892 | "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
1893 | "dev": true,
1894 | "dependencies": {
1895 | "type-fest": "^0.20.2"
1896 | },
1897 | "engines": {
1898 | "node": ">=8"
1899 | },
1900 | "funding": {
1901 | "url": "https://github.com/sponsors/sindresorhus"
1902 | }
1903 | },
1904 | "node_modules/globby": {
1905 | "version": "11.1.0",
1906 | "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
1907 | "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
1908 | "dev": true,
1909 | "dependencies": {
1910 | "array-union": "^2.1.0",
1911 | "dir-glob": "^3.0.1",
1912 | "fast-glob": "^3.2.9",
1913 | "ignore": "^5.2.0",
1914 | "merge2": "^1.4.1",
1915 | "slash": "^3.0.0"
1916 | },
1917 | "engines": {
1918 | "node": ">=10"
1919 | },
1920 | "funding": {
1921 | "url": "https://github.com/sponsors/sindresorhus"
1922 | }
1923 | },
1924 | "node_modules/graphemer": {
1925 | "version": "1.4.0",
1926 | "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
1927 | "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
1928 | "dev": true
1929 | },
1930 | "node_modules/has-flag": {
1931 | "version": "4.0.0",
1932 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
1933 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
1934 | "dev": true,
1935 | "engines": {
1936 | "node": ">=8"
1937 | }
1938 | },
1939 | "node_modules/ignore": {
1940 | "version": "5.3.1",
1941 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
1942 | "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
1943 | "dev": true,
1944 | "engines": {
1945 | "node": ">= 4"
1946 | }
1947 | },
1948 | "node_modules/import-fresh": {
1949 | "version": "3.3.0",
1950 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
1951 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
1952 | "dev": true,
1953 | "dependencies": {
1954 | "parent-module": "^1.0.0",
1955 | "resolve-from": "^4.0.0"
1956 | },
1957 | "engines": {
1958 | "node": ">=6"
1959 | },
1960 | "funding": {
1961 | "url": "https://github.com/sponsors/sindresorhus"
1962 | }
1963 | },
1964 | "node_modules/imurmurhash": {
1965 | "version": "0.1.4",
1966 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
1967 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
1968 | "dev": true,
1969 | "engines": {
1970 | "node": ">=0.8.19"
1971 | }
1972 | },
1973 | "node_modules/inflight": {
1974 | "version": "1.0.6",
1975 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
1976 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
1977 | "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
1978 | "dev": true,
1979 | "dependencies": {
1980 | "once": "^1.3.0",
1981 | "wrappy": "1"
1982 | }
1983 | },
1984 | "node_modules/inherits": {
1985 | "version": "2.0.4",
1986 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1987 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1988 | "dev": true
1989 | },
1990 | "node_modules/is-extglob": {
1991 | "version": "2.1.1",
1992 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1993 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1994 | "dev": true,
1995 | "engines": {
1996 | "node": ">=0.10.0"
1997 | }
1998 | },
1999 | "node_modules/is-glob": {
2000 | "version": "4.0.3",
2001 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
2002 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
2003 | "dev": true,
2004 | "dependencies": {
2005 | "is-extglob": "^2.1.1"
2006 | },
2007 | "engines": {
2008 | "node": ">=0.10.0"
2009 | }
2010 | },
2011 | "node_modules/is-number": {
2012 | "version": "7.0.0",
2013 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
2014 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
2015 | "dev": true,
2016 | "engines": {
2017 | "node": ">=0.12.0"
2018 | }
2019 | },
2020 | "node_modules/is-path-inside": {
2021 | "version": "3.0.3",
2022 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
2023 | "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
2024 | "dev": true,
2025 | "engines": {
2026 | "node": ">=8"
2027 | }
2028 | },
2029 | "node_modules/isexe": {
2030 | "version": "2.0.0",
2031 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
2032 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
2033 | "dev": true
2034 | },
2035 | "node_modules/js-tokens": {
2036 | "version": "4.0.0",
2037 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
2038 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
2039 | },
2040 | "node_modules/js-yaml": {
2041 | "version": "4.1.0",
2042 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
2043 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
2044 | "dev": true,
2045 | "dependencies": {
2046 | "argparse": "^2.0.1"
2047 | },
2048 | "bin": {
2049 | "js-yaml": "bin/js-yaml.js"
2050 | }
2051 | },
2052 | "node_modules/json-buffer": {
2053 | "version": "3.0.1",
2054 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
2055 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
2056 | "dev": true
2057 | },
2058 | "node_modules/json-schema-traverse": {
2059 | "version": "0.4.1",
2060 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
2061 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
2062 | "dev": true
2063 | },
2064 | "node_modules/json-stable-stringify-without-jsonify": {
2065 | "version": "1.0.1",
2066 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
2067 | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
2068 | "dev": true
2069 | },
2070 | "node_modules/keyv": {
2071 | "version": "4.5.4",
2072 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
2073 | "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
2074 | "dev": true,
2075 | "dependencies": {
2076 | "json-buffer": "3.0.1"
2077 | }
2078 | },
2079 | "node_modules/levn": {
2080 | "version": "0.4.1",
2081 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
2082 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
2083 | "dev": true,
2084 | "dependencies": {
2085 | "prelude-ls": "^1.2.1",
2086 | "type-check": "~0.4.0"
2087 | },
2088 | "engines": {
2089 | "node": ">= 0.8.0"
2090 | }
2091 | },
2092 | "node_modules/locate-path": {
2093 | "version": "6.0.0",
2094 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
2095 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
2096 | "dev": true,
2097 | "dependencies": {
2098 | "p-locate": "^5.0.0"
2099 | },
2100 | "engines": {
2101 | "node": ">=10"
2102 | },
2103 | "funding": {
2104 | "url": "https://github.com/sponsors/sindresorhus"
2105 | }
2106 | },
2107 | "node_modules/lodash.merge": {
2108 | "version": "4.6.2",
2109 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
2110 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
2111 | "dev": true
2112 | },
2113 | "node_modules/loose-envify": {
2114 | "version": "1.4.0",
2115 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
2116 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
2117 | "dependencies": {
2118 | "js-tokens": "^3.0.0 || ^4.0.0"
2119 | },
2120 | "bin": {
2121 | "loose-envify": "cli.js"
2122 | }
2123 | },
2124 | "node_modules/merge2": {
2125 | "version": "1.4.1",
2126 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
2127 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
2128 | "dev": true,
2129 | "engines": {
2130 | "node": ">= 8"
2131 | }
2132 | },
2133 | "node_modules/micromatch": {
2134 | "version": "4.0.7",
2135 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
2136 | "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
2137 | "dev": true,
2138 | "dependencies": {
2139 | "braces": "^3.0.3",
2140 | "picomatch": "^2.3.1"
2141 | },
2142 | "engines": {
2143 | "node": ">=8.6"
2144 | }
2145 | },
2146 | "node_modules/minimatch": {
2147 | "version": "9.0.4",
2148 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
2149 | "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
2150 | "dev": true,
2151 | "dependencies": {
2152 | "brace-expansion": "^2.0.1"
2153 | },
2154 | "engines": {
2155 | "node": ">=16 || 14 >=14.17"
2156 | },
2157 | "funding": {
2158 | "url": "https://github.com/sponsors/isaacs"
2159 | }
2160 | },
2161 | "node_modules/ms": {
2162 | "version": "2.1.2",
2163 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
2164 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
2165 | "dev": true
2166 | },
2167 | "node_modules/nanoid": {
2168 | "version": "3.3.7",
2169 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
2170 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
2171 | "dev": true,
2172 | "funding": [
2173 | {
2174 | "type": "github",
2175 | "url": "https://github.com/sponsors/ai"
2176 | }
2177 | ],
2178 | "bin": {
2179 | "nanoid": "bin/nanoid.cjs"
2180 | },
2181 | "engines": {
2182 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2183 | }
2184 | },
2185 | "node_modules/natural-compare": {
2186 | "version": "1.4.0",
2187 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
2188 | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
2189 | "dev": true
2190 | },
2191 | "node_modules/once": {
2192 | "version": "1.4.0",
2193 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
2194 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
2195 | "dev": true,
2196 | "dependencies": {
2197 | "wrappy": "1"
2198 | }
2199 | },
2200 | "node_modules/optionator": {
2201 | "version": "0.9.4",
2202 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
2203 | "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
2204 | "dev": true,
2205 | "dependencies": {
2206 | "deep-is": "^0.1.3",
2207 | "fast-levenshtein": "^2.0.6",
2208 | "levn": "^0.4.1",
2209 | "prelude-ls": "^1.2.1",
2210 | "type-check": "^0.4.0",
2211 | "word-wrap": "^1.2.5"
2212 | },
2213 | "engines": {
2214 | "node": ">= 0.8.0"
2215 | }
2216 | },
2217 | "node_modules/p-limit": {
2218 | "version": "3.1.0",
2219 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
2220 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
2221 | "dev": true,
2222 | "dependencies": {
2223 | "yocto-queue": "^0.1.0"
2224 | },
2225 | "engines": {
2226 | "node": ">=10"
2227 | },
2228 | "funding": {
2229 | "url": "https://github.com/sponsors/sindresorhus"
2230 | }
2231 | },
2232 | "node_modules/p-locate": {
2233 | "version": "5.0.0",
2234 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
2235 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
2236 | "dev": true,
2237 | "dependencies": {
2238 | "p-limit": "^3.0.2"
2239 | },
2240 | "engines": {
2241 | "node": ">=10"
2242 | },
2243 | "funding": {
2244 | "url": "https://github.com/sponsors/sindresorhus"
2245 | }
2246 | },
2247 | "node_modules/parent-module": {
2248 | "version": "1.0.1",
2249 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
2250 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
2251 | "dev": true,
2252 | "dependencies": {
2253 | "callsites": "^3.0.0"
2254 | },
2255 | "engines": {
2256 | "node": ">=6"
2257 | }
2258 | },
2259 | "node_modules/path-exists": {
2260 | "version": "4.0.0",
2261 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
2262 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
2263 | "dev": true,
2264 | "engines": {
2265 | "node": ">=8"
2266 | }
2267 | },
2268 | "node_modules/path-is-absolute": {
2269 | "version": "1.0.1",
2270 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
2271 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
2272 | "dev": true,
2273 | "engines": {
2274 | "node": ">=0.10.0"
2275 | }
2276 | },
2277 | "node_modules/path-key": {
2278 | "version": "3.1.1",
2279 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2280 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2281 | "dev": true,
2282 | "engines": {
2283 | "node": ">=8"
2284 | }
2285 | },
2286 | "node_modules/path-type": {
2287 | "version": "4.0.0",
2288 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
2289 | "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
2290 | "dev": true,
2291 | "engines": {
2292 | "node": ">=8"
2293 | }
2294 | },
2295 | "node_modules/picocolors": {
2296 | "version": "1.0.1",
2297 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
2298 | "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
2299 | "dev": true
2300 | },
2301 | "node_modules/picomatch": {
2302 | "version": "2.3.1",
2303 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
2304 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
2305 | "dev": true,
2306 | "engines": {
2307 | "node": ">=8.6"
2308 | },
2309 | "funding": {
2310 | "url": "https://github.com/sponsors/jonschlinkert"
2311 | }
2312 | },
2313 | "node_modules/postcss": {
2314 | "version": "8.4.38",
2315 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
2316 | "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
2317 | "dev": true,
2318 | "funding": [
2319 | {
2320 | "type": "opencollective",
2321 | "url": "https://opencollective.com/postcss/"
2322 | },
2323 | {
2324 | "type": "tidelift",
2325 | "url": "https://tidelift.com/funding/github/npm/postcss"
2326 | },
2327 | {
2328 | "type": "github",
2329 | "url": "https://github.com/sponsors/ai"
2330 | }
2331 | ],
2332 | "dependencies": {
2333 | "nanoid": "^3.3.7",
2334 | "picocolors": "^1.0.0",
2335 | "source-map-js": "^1.2.0"
2336 | },
2337 | "engines": {
2338 | "node": "^10 || ^12 || >=14"
2339 | }
2340 | },
2341 | "node_modules/prelude-ls": {
2342 | "version": "1.2.1",
2343 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
2344 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
2345 | "dev": true,
2346 | "engines": {
2347 | "node": ">= 0.8.0"
2348 | }
2349 | },
2350 | "node_modules/punycode": {
2351 | "version": "2.3.1",
2352 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
2353 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
2354 | "dev": true,
2355 | "engines": {
2356 | "node": ">=6"
2357 | }
2358 | },
2359 | "node_modules/queue-microtask": {
2360 | "version": "1.2.3",
2361 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
2362 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
2363 | "dev": true,
2364 | "funding": [
2365 | {
2366 | "type": "github",
2367 | "url": "https://github.com/sponsors/feross"
2368 | },
2369 | {
2370 | "type": "patreon",
2371 | "url": "https://www.patreon.com/feross"
2372 | },
2373 | {
2374 | "type": "consulting",
2375 | "url": "https://feross.org/support"
2376 | }
2377 | ]
2378 | },
2379 | "node_modules/react": {
2380 | "version": "18.3.1",
2381 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
2382 | "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
2383 | "dependencies": {
2384 | "loose-envify": "^1.1.0"
2385 | },
2386 | "engines": {
2387 | "node": ">=0.10.0"
2388 | }
2389 | },
2390 | "node_modules/react-dom": {
2391 | "version": "18.3.1",
2392 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
2393 | "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
2394 | "dependencies": {
2395 | "loose-envify": "^1.1.0",
2396 | "scheduler": "^0.23.2"
2397 | },
2398 | "peerDependencies": {
2399 | "react": "^18.3.1"
2400 | }
2401 | },
2402 | "node_modules/react-tweet": {
2403 | "version": "3.2.1",
2404 | "resolved": "https://registry.npmjs.org/react-tweet/-/react-tweet-3.2.1.tgz",
2405 | "integrity": "sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA==",
2406 | "dependencies": {
2407 | "@swc/helpers": "^0.5.3",
2408 | "clsx": "^2.0.0",
2409 | "swr": "^2.2.4"
2410 | },
2411 | "peerDependencies": {
2412 | "react": ">= 18.0.0",
2413 | "react-dom": ">= 18.0.0"
2414 | }
2415 | },
2416 | "node_modules/resolve-from": {
2417 | "version": "4.0.0",
2418 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
2419 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
2420 | "dev": true,
2421 | "engines": {
2422 | "node": ">=4"
2423 | }
2424 | },
2425 | "node_modules/reusify": {
2426 | "version": "1.0.4",
2427 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
2428 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
2429 | "dev": true,
2430 | "engines": {
2431 | "iojs": ">=1.0.0",
2432 | "node": ">=0.10.0"
2433 | }
2434 | },
2435 | "node_modules/rimraf": {
2436 | "version": "3.0.2",
2437 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
2438 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
2439 | "deprecated": "Rimraf versions prior to v4 are no longer supported",
2440 | "dev": true,
2441 | "dependencies": {
2442 | "glob": "^7.1.3"
2443 | },
2444 | "bin": {
2445 | "rimraf": "bin.js"
2446 | },
2447 | "funding": {
2448 | "url": "https://github.com/sponsors/isaacs"
2449 | }
2450 | },
2451 | "node_modules/rollup": {
2452 | "version": "4.18.0",
2453 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
2454 | "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
2455 | "dev": true,
2456 | "dependencies": {
2457 | "@types/estree": "1.0.5"
2458 | },
2459 | "bin": {
2460 | "rollup": "dist/bin/rollup"
2461 | },
2462 | "engines": {
2463 | "node": ">=18.0.0",
2464 | "npm": ">=8.0.0"
2465 | },
2466 | "optionalDependencies": {
2467 | "@rollup/rollup-android-arm-eabi": "4.18.0",
2468 | "@rollup/rollup-android-arm64": "4.18.0",
2469 | "@rollup/rollup-darwin-arm64": "4.18.0",
2470 | "@rollup/rollup-darwin-x64": "4.18.0",
2471 | "@rollup/rollup-linux-arm-gnueabihf": "4.18.0",
2472 | "@rollup/rollup-linux-arm-musleabihf": "4.18.0",
2473 | "@rollup/rollup-linux-arm64-gnu": "4.18.0",
2474 | "@rollup/rollup-linux-arm64-musl": "4.18.0",
2475 | "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0",
2476 | "@rollup/rollup-linux-riscv64-gnu": "4.18.0",
2477 | "@rollup/rollup-linux-s390x-gnu": "4.18.0",
2478 | "@rollup/rollup-linux-x64-gnu": "4.18.0",
2479 | "@rollup/rollup-linux-x64-musl": "4.18.0",
2480 | "@rollup/rollup-win32-arm64-msvc": "4.18.0",
2481 | "@rollup/rollup-win32-ia32-msvc": "4.18.0",
2482 | "@rollup/rollup-win32-x64-msvc": "4.18.0",
2483 | "fsevents": "~2.3.2"
2484 | }
2485 | },
2486 | "node_modules/run-parallel": {
2487 | "version": "1.2.0",
2488 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
2489 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
2490 | "dev": true,
2491 | "funding": [
2492 | {
2493 | "type": "github",
2494 | "url": "https://github.com/sponsors/feross"
2495 | },
2496 | {
2497 | "type": "patreon",
2498 | "url": "https://www.patreon.com/feross"
2499 | },
2500 | {
2501 | "type": "consulting",
2502 | "url": "https://feross.org/support"
2503 | }
2504 | ],
2505 | "dependencies": {
2506 | "queue-microtask": "^1.2.2"
2507 | }
2508 | },
2509 | "node_modules/scheduler": {
2510 | "version": "0.23.2",
2511 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
2512 | "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
2513 | "dependencies": {
2514 | "loose-envify": "^1.1.0"
2515 | }
2516 | },
2517 | "node_modules/semver": {
2518 | "version": "7.6.2",
2519 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
2520 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
2521 | "dev": true,
2522 | "bin": {
2523 | "semver": "bin/semver.js"
2524 | },
2525 | "engines": {
2526 | "node": ">=10"
2527 | }
2528 | },
2529 | "node_modules/shebang-command": {
2530 | "version": "2.0.0",
2531 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2532 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2533 | "dev": true,
2534 | "dependencies": {
2535 | "shebang-regex": "^3.0.0"
2536 | },
2537 | "engines": {
2538 | "node": ">=8"
2539 | }
2540 | },
2541 | "node_modules/shebang-regex": {
2542 | "version": "3.0.0",
2543 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2544 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2545 | "dev": true,
2546 | "engines": {
2547 | "node": ">=8"
2548 | }
2549 | },
2550 | "node_modules/slash": {
2551 | "version": "3.0.0",
2552 | "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
2553 | "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
2554 | "dev": true,
2555 | "engines": {
2556 | "node": ">=8"
2557 | }
2558 | },
2559 | "node_modules/source-map-js": {
2560 | "version": "1.2.0",
2561 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
2562 | "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
2563 | "dev": true,
2564 | "engines": {
2565 | "node": ">=0.10.0"
2566 | }
2567 | },
2568 | "node_modules/strip-ansi": {
2569 | "version": "6.0.1",
2570 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2571 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2572 | "dev": true,
2573 | "dependencies": {
2574 | "ansi-regex": "^5.0.1"
2575 | },
2576 | "engines": {
2577 | "node": ">=8"
2578 | }
2579 | },
2580 | "node_modules/strip-json-comments": {
2581 | "version": "3.1.1",
2582 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
2583 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
2584 | "dev": true,
2585 | "engines": {
2586 | "node": ">=8"
2587 | },
2588 | "funding": {
2589 | "url": "https://github.com/sponsors/sindresorhus"
2590 | }
2591 | },
2592 | "node_modules/supports-color": {
2593 | "version": "7.2.0",
2594 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
2595 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
2596 | "dev": true,
2597 | "dependencies": {
2598 | "has-flag": "^4.0.0"
2599 | },
2600 | "engines": {
2601 | "node": ">=8"
2602 | }
2603 | },
2604 | "node_modules/swr": {
2605 | "version": "2.2.5",
2606 | "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
2607 | "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
2608 | "dependencies": {
2609 | "client-only": "^0.0.1",
2610 | "use-sync-external-store": "^1.2.0"
2611 | },
2612 | "peerDependencies": {
2613 | "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
2614 | }
2615 | },
2616 | "node_modules/text-table": {
2617 | "version": "0.2.0",
2618 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
2619 | "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
2620 | "dev": true
2621 | },
2622 | "node_modules/to-regex-range": {
2623 | "version": "5.0.1",
2624 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2625 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2626 | "dev": true,
2627 | "dependencies": {
2628 | "is-number": "^7.0.0"
2629 | },
2630 | "engines": {
2631 | "node": ">=8.0"
2632 | }
2633 | },
2634 | "node_modules/ts-api-utils": {
2635 | "version": "1.3.0",
2636 | "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
2637 | "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
2638 | "dev": true,
2639 | "engines": {
2640 | "node": ">=16"
2641 | },
2642 | "peerDependencies": {
2643 | "typescript": ">=4.2.0"
2644 | }
2645 | },
2646 | "node_modules/tslib": {
2647 | "version": "2.6.3",
2648 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
2649 | "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
2650 | },
2651 | "node_modules/type-check": {
2652 | "version": "0.4.0",
2653 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
2654 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
2655 | "dev": true,
2656 | "dependencies": {
2657 | "prelude-ls": "^1.2.1"
2658 | },
2659 | "engines": {
2660 | "node": ">= 0.8.0"
2661 | }
2662 | },
2663 | "node_modules/type-fest": {
2664 | "version": "0.20.2",
2665 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
2666 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
2667 | "dev": true,
2668 | "engines": {
2669 | "node": ">=10"
2670 | },
2671 | "funding": {
2672 | "url": "https://github.com/sponsors/sindresorhus"
2673 | }
2674 | },
2675 | "node_modules/typescript": {
2676 | "version": "5.4.5",
2677 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
2678 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
2679 | "dev": true,
2680 | "bin": {
2681 | "tsc": "bin/tsc",
2682 | "tsserver": "bin/tsserver"
2683 | },
2684 | "engines": {
2685 | "node": ">=14.17"
2686 | }
2687 | },
2688 | "node_modules/uri-js": {
2689 | "version": "4.4.1",
2690 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
2691 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
2692 | "dev": true,
2693 | "dependencies": {
2694 | "punycode": "^2.1.0"
2695 | }
2696 | },
2697 | "node_modules/use-sync-external-store": {
2698 | "version": "1.2.2",
2699 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
2700 | "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
2701 | "peerDependencies": {
2702 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
2703 | }
2704 | },
2705 | "node_modules/vite": {
2706 | "version": "5.3.1",
2707 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz",
2708 | "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==",
2709 | "dev": true,
2710 | "dependencies": {
2711 | "esbuild": "^0.21.3",
2712 | "postcss": "^8.4.38",
2713 | "rollup": "^4.13.0"
2714 | },
2715 | "bin": {
2716 | "vite": "bin/vite.js"
2717 | },
2718 | "engines": {
2719 | "node": "^18.0.0 || >=20.0.0"
2720 | },
2721 | "funding": {
2722 | "url": "https://github.com/vitejs/vite?sponsor=1"
2723 | },
2724 | "optionalDependencies": {
2725 | "fsevents": "~2.3.3"
2726 | },
2727 | "peerDependencies": {
2728 | "@types/node": "^18.0.0 || >=20.0.0",
2729 | "less": "*",
2730 | "lightningcss": "^1.21.0",
2731 | "sass": "*",
2732 | "stylus": "*",
2733 | "sugarss": "*",
2734 | "terser": "^5.4.0"
2735 | },
2736 | "peerDependenciesMeta": {
2737 | "@types/node": {
2738 | "optional": true
2739 | },
2740 | "less": {
2741 | "optional": true
2742 | },
2743 | "lightningcss": {
2744 | "optional": true
2745 | },
2746 | "sass": {
2747 | "optional": true
2748 | },
2749 | "stylus": {
2750 | "optional": true
2751 | },
2752 | "sugarss": {
2753 | "optional": true
2754 | },
2755 | "terser": {
2756 | "optional": true
2757 | }
2758 | }
2759 | },
2760 | "node_modules/which": {
2761 | "version": "2.0.2",
2762 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2763 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2764 | "dev": true,
2765 | "dependencies": {
2766 | "isexe": "^2.0.0"
2767 | },
2768 | "bin": {
2769 | "node-which": "bin/node-which"
2770 | },
2771 | "engines": {
2772 | "node": ">= 8"
2773 | }
2774 | },
2775 | "node_modules/word-wrap": {
2776 | "version": "1.2.5",
2777 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
2778 | "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
2779 | "dev": true,
2780 | "engines": {
2781 | "node": ">=0.10.0"
2782 | }
2783 | },
2784 | "node_modules/wrappy": {
2785 | "version": "1.0.2",
2786 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
2787 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2788 | "dev": true
2789 | },
2790 | "node_modules/wretch": {
2791 | "version": "2.9.0",
2792 | "resolved": "https://registry.npmjs.org/wretch/-/wretch-2.9.0.tgz",
2793 | "integrity": "sha512-kKp1xWQO+Vh9I6RJq7Yers2KKrmF7LXB00c9knMDn3IdB7etcQOarrsHArpj1sZ1Dlav8ss6R1DuUpDSTqmQew==",
2794 | "engines": {
2795 | "node": ">=14"
2796 | }
2797 | },
2798 | "node_modules/yocto-queue": {
2799 | "version": "0.1.0",
2800 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
2801 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
2802 | "dev": true,
2803 | "engines": {
2804 | "node": ">=10"
2805 | },
2806 | "funding": {
2807 | "url": "https://github.com/sponsors/sindresorhus"
2808 | }
2809 | }
2810 | }
2811 | }
2812 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "publikes-ui",
3 | "private": true,
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "tsc && vite build",
10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "react-tweet": "^3.2.1",
17 | "swr": "^2.2.5",
18 | "wretch": "^2.9.0"
19 | },
20 | "devDependencies": {
21 | "@types/react": "^18.2.66",
22 | "@types/react-dom": "^18.2.22",
23 | "@typescript-eslint/eslint-plugin": "^7.2.0",
24 | "@typescript-eslint/parser": "^7.2.0",
25 | "@vitejs/plugin-react-swc": "^3.5.0",
26 | "eslint": "^8.57.0",
27 | "eslint-plugin-react-hooks": "^4.6.0",
28 | "eslint-plugin-react-refresh": "^0.4.6",
29 | "typescript": "^5.2.2",
30 | "vite": "^5.2.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/Api.ts:
--------------------------------------------------------------------------------
1 | import useSWRInfinite from "swr/infinite";
2 | import wretch from "wretch";
3 |
4 | export type Current = {
5 | head: string;
6 | last: string | null;
7 | updated_at: number;
8 | };
9 |
10 | export type PageId = string;
11 | export type Batch = {
12 | id: string;
13 | head: boolean;
14 | pages: PageId[];
15 | next: string | null;
16 | created_at: number;
17 | updated_at: number;
18 | updated_nonce?: string;
19 | };
20 |
21 | export type Page = {
22 | id: string;
23 | statuses: PageStatusItem[];
24 | created_at?: number;
25 | virtual?: true;
26 | };
27 |
28 | export type PageStatusItem = {
29 | id: string;
30 | ts?: number;
31 | };
32 |
33 | export async function fetchCurrent(): Promise {
34 | return await wretch(`${import.meta.env.VITE_DATA_URL}/current.json`)
35 | .get()
36 | .json();
37 | }
38 |
39 | export async function fetchBatch(batchId: string): Promise {
40 | return await wretch(
41 | `${import.meta.env.VITE_DATA_URL}/batches/${batchId}.json`
42 | )
43 | .get()
44 | .json();
45 | }
46 |
47 | const HEAD_PAGE_PATTERN = /^head\/[^/]+\/([0-9]+)$/;
48 | export async function fetchPage(pageId: PageId): Promise {
49 | const m = pageId.match(HEAD_PAGE_PATTERN);
50 | if (m && m[1]) {
51 | return {
52 | id: pageId,
53 | statuses: [{ id: m[1] }],
54 | virtual: true,
55 | };
56 | }
57 |
58 | return await wretch(`${import.meta.env.VITE_DATA_URL}/pages/${pageId}.json`)
59 | .get()
60 | .json();
61 | }
62 |
63 | export async function fetchFirstPageOrMakeVirtual(
64 | batch: Batch
65 | ): Promise {
66 | // Return head batch as a single virtual page
67 | if (batch.head) {
68 | return {
69 | batch: { ...batch, pages: [`head-virtual/${batch.id}`] },
70 | page: await singleVirtualPage(batch),
71 | };
72 | }
73 | return {
74 | batch,
75 | page: await fetchPage(batch.pages[0]),
76 | };
77 | }
78 |
79 | export type PageFetcherResponse =
80 | | { batch: Batch; page: Page }
81 | | { batch: null; empty: true };
82 | export type PageFetcherKey = ["publikes-swr", string | null, PageId | null];
83 |
84 | async function loadFirstPage(
85 | startBatch: string | null
86 | ): Promise<[Batch, Current | undefined]> {
87 | if (startBatch) {
88 | return [await fetchBatch(startBatch), undefined];
89 | } else {
90 | const current = await fetchCurrent();
91 | return [await fetchBatch(current.head), current];
92 | }
93 | }
94 |
95 | function getPageKey(
96 | startBatch: string | undefined,
97 | index: number,
98 | previousPageData?: PageFetcherResponse
99 | ): PageFetcherKey | null {
100 | if (index === 0) {
101 | // Load first page and first batch
102 | return ["publikes-swr", startBatch || null, null];
103 | }
104 | if (previousPageData?.batch?.head) {
105 | // Head is emulated as a single page, force load next batch
106 | return ["publikes-swr", previousPageData.batch.next || "@LAST", null];
107 | }
108 | if (previousPageData?.batch) {
109 | // Load next page in the same batch
110 | const idx = previousPageData.batch.pages.indexOf(previousPageData.page.id);
111 | if (idx >= 0 && previousPageData.batch.pages[idx + 1]) {
112 | return [
113 | "publikes-swr",
114 | previousPageData.batch.id,
115 | previousPageData.batch.pages[idx + 1],
116 | ];
117 | }
118 | }
119 | if (previousPageData?.batch?.next) {
120 | return ["publikes-swr", previousPageData?.batch?.next, null];
121 | }
122 | return null;
123 | }
124 |
125 | async function pageFetcher(key: PageFetcherKey): Promise {
126 | const [, batchKey, pageKey] = key;
127 | console.log("pageFetcher > batchKey/pageKey", batchKey, pageKey);
128 | if (!batchKey) {
129 | // Load first page and first batch
130 | const [batch, maybeCurrent] = await loadFirstPage(batchKey);
131 |
132 | if (batch.pages[0]) {
133 | console.log("loadFirstPage > fetchFirstPageOrMakeVirtual", batch.pages);
134 | return await fetchFirstPageOrMakeVirtual(batch);
135 | } else if (batch.head && !batch.next) {
136 | console.log("loadFirstPage > empty head", batch.pages);
137 | const current = maybeCurrent ?? (await fetchCurrent());
138 | if (current.last)
139 | return pageFetcher(["publikes-swr", current.last, null]);
140 | } else if (batch.next) {
141 | return pageFetcher(["publikes-swr", batch.next, null]);
142 | }
143 |
144 | console.warn("Possibly empty");
145 | return { batch: null, empty: true };
146 | }
147 |
148 | if (batchKey && pageKey) {
149 | // Load next page in the same batch
150 | return {
151 | batch: await fetchBatch(batchKey),
152 | page: await fetchPage(pageKey),
153 | };
154 | }
155 | if (batchKey) {
156 | // Load next batch and its first page
157 | const batchId =
158 | batchKey == "@LAST" ? (await fetchCurrent()).last : batchKey;
159 | if (!batchId) return { batch: null, empty: true };
160 |
161 | let batch = await fetchBatch(batchId);
162 | while (batch.pages.length < 1 && batch.next) {
163 | batch = await fetchBatch(batch?.next);
164 | }
165 |
166 | if (batch.pages[0]) {
167 | return await fetchFirstPageOrMakeVirtual(batch);
168 | }
169 | }
170 |
171 | console.warn("Last Resort Empty");
172 | return { batch: null, empty: true };
173 | }
174 |
175 | async function singleVirtualPage(batch: Batch): Promise {
176 | console.log("singleVirtualPage", batch);
177 | return {
178 | id: `head-virtual/${batch.id}`,
179 | virtual: true,
180 | statuses: (await Promise.all(batch.pages.map((p) => fetchPage(p)))).flatMap(
181 | (p) => p.statuses
182 | ),
183 | };
184 | }
185 |
186 | export function usePageInfinite(startBatch?: string) {
187 | return useSWRInfinite(
188 | (index, previousPageData) => {
189 | const retval = getPageKey(startBatch, index, previousPageData);
190 | console.log("pageKey", { startBatch, index, previousPageData }, retval);
191 | return retval;
192 | },
193 | pageFetcher,
194 | {
195 | initialSize: 1,
196 | revalidateIfStale: false,
197 | revalidateOnMount: true,
198 | revalidateOnReconnect: false,
199 | revalidateFirstPage: false,
200 | }
201 | );
202 | }
203 |
--------------------------------------------------------------------------------
/ui/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | text-align: center;
4 | color: #0f1419;
5 | }
6 |
7 | #root {
8 | max-width: 550px;
9 | margin-left: auto;
10 | margin-right: auto;
11 | text-align: left;
12 | }
13 |
14 | #root > header h1 {
15 | font-size: 24px;
16 | margin-bottom: 8px;
17 | }
18 |
19 | .powered-by {
20 | margin: 0;
21 | font-size: 12px;
22 | color: #506471;
23 | }
24 |
25 | .past-page-warning {
26 | text-align: center;
27 | }
28 |
29 | .load-more {
30 | height: 30px;
31 | font-size: 25px;
32 | text-align: center;
33 | display: flex;
34 | flex-direction: row;
35 | justify-content: space-around;
36 | padding-bottom: 24px;
37 | }
38 |
39 | .load-more p {
40 | margin: 0;
41 | }
42 |
43 | .load-more p.last-page {
44 | font-size: 18px;
45 | }
46 |
47 | /* We miss Firefox. Sorry about that. */
48 | @scope (#root) to (.liked-tweet) {
49 | a, a:visited {
50 | color: #006fd6;
51 | }
52 | }
53 |
54 | @media (prefers-color-scheme: dark) {
55 | body {
56 | background-color: #15202b;
57 | scrollbar-color: #5c6e7e,#1e2732;
58 | color: #ffffff;
59 | }
60 | @scope (#root) to (.liked-tweet) {
61 | a, a:visited {
62 | color: #1d9bf0;
63 | }
64 | }
65 | .powered-by {
66 | color: #8b98a5;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ui/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useRef } from "react";
2 | import { usePageInfinite } from "./Api";
3 | import "./App.css";
4 | import { Tweet } from "react-tweet";
5 |
6 | function App() {
7 | const startBatch = useMemo(() => {
8 | const search = new URLSearchParams(location.search);
9 | return search.get("batch") || undefined;
10 | }, []);
11 | const { data, error, isLoading, isValidating, size, setSize } =
12 | usePageInfinite(startBatch);
13 |
14 | const observer = useInteractionObserver(
15 | useCallback(
16 | (entries) => {
17 | if (isValidating || isLoading) return;
18 | entries.forEach((entry) => {
19 | if (entry.isIntersecting) setSize(size + 1);
20 | });
21 | },
22 | [isValidating, isLoading, size, setSize]
23 | ),
24 | {}
25 | );
26 |
27 | useEffect(() => {
28 | if (!data) return;
29 | if (size < 2) return;
30 | const batchId = data[data.length - 1]?.batch?.id;
31 | if (batchId) {
32 | history.replaceState(
33 | {},
34 | "",
35 | `${location.pathname}?batch=${encodeURIComponent(batchId)}`
36 | );
37 | }
38 | }, [size, data]);
39 |
40 | const lastPage = data ? data[data.length - 1] : undefined;
41 | const reachedLastPage =
42 | !isLoading &&
43 | !isValidating &&
44 | lastPage &&
45 | (lastPage.batch ? !lastPage.batch.head && !lastPage.batch.next : true);
46 |
47 | useEffect(() => {}, []);
48 |
49 | return (
50 | <>
51 |
58 | {startBatch ? (
59 |
60 | Browsing the past page ({startBatch}).
61 |
62 | Back to the most recent like
63 |
64 | ) : null}
65 |
66 | {data?.map((b) => {
67 | if (!b.batch) return null;
68 | const page = b.page;
69 | return (
70 |
71 | {page.statuses.map((status) => {
72 | return (
73 |
78 |
79 |
80 | );
81 | })}
82 |
83 | );
84 | })}
85 |
86 | {error ? Error: {error}
: null}
87 |
88 | {reachedLastPage ? (
89 |
You've reached the last page.
90 | ) : null}
91 | {isLoading || isValidating ?
👊
: null}
92 |
93 | >
94 | );
95 | }
96 |
97 | function useInteractionObserver(
98 | callback: IntersectionObserverCallback,
99 | options: IntersectionObserverInit
100 | ) {
101 | const ref = useRef(null);
102 | const observer = useMemo(
103 | () => new IntersectionObserver(callback, options),
104 | [callback, options]
105 | );
106 | useEffect(() => {
107 | const elem = ref.current;
108 | if (!elem) return;
109 | observer.observe(elem);
110 | return () => observer.unobserve(elem);
111 | }, [observer, ref]);
112 | return ref;
113 | }
114 |
115 | export default App;
116 |
--------------------------------------------------------------------------------
/ui/src/index.css:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/ui/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | interface ImportMetaEnv {
3 | readonly VITE_DATA_URL: string;
4 | readonly VITE_HTML_TITLE: string;
5 | readonly VITE_HTML_LANG: string;
6 | readonly VITE_APP_LANG: string;
7 | }
8 | interface ImportMeta {
9 | readonly env: ImportMetaEnv;
10 | }
11 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/ui/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/variables.tf:
--------------------------------------------------------------------------------
1 | variable "iam_role_prefix" {
2 | type = string
3 | }
4 | variable "name_prefix" {
5 | type = string
6 | }
7 | variable "s3_bucket_name" {
8 | type = string
9 | }
10 |
11 | variable "app_domain" {
12 | type = string
13 | }
14 | variable "certificate_arn" {
15 | type = string
16 | }
17 | variable "cloudfront_log_bucket" {
18 | type = string
19 | }
20 | variable "cloudfront_log_prefix" {
21 | type = string
22 | }
23 |
24 | variable "lambda_env_vars" {
25 | type = map(string)
26 | default = {}
27 | }
28 |
--------------------------------------------------------------------------------
/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------