diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..baa1ea2 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,217 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now) + {Credo.Check.Refactor.UtcNowTruncate, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.formatter.exs b/.formatter.exs index d2cda26..3520796 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,5 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + locals_without_parens: [from: 2], ] diff --git a/.gitignore b/.gitignore index 8dc9689..918bb26 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,16 @@ hipdster-*.tar # Temporary files, for example, from tests. /tmp/ *.so +auth.ets +.mnesia/ + +# Cargo build output +/target/ + +# SQLite +pds +pds-shm +pds-wal # Cargo build output /target/ diff --git a/README.md b/README.md index e2e8769..9100c6a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,24 @@ # hipdster An ATProto PDS in Elixir/Rust -## Installation +## The current state of things -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `hipdster` to your list of dependencies in `mix.exs`: +As of right now, this is not in a state where it can be used yet. + +Statuses for various components: + - Identity resolution - more or less complete + - cryptographic key generation, signing, and validation - complete (secp256k1 only) + - DID PLC operation signing (CBOR) - creates complete, probably easy to add updates from here + - transforming between JSON and CBOR - complete + - Lexicon validation - not currently planned + - MST - started + - Firehose - not started + - TID generation (and decoding!!) - complete + - blobs - getBlob, listBlobs - uploadBlob held up by auth/jwt + - service proxy header - parses and finds service URL + - inter-service auth - need JWT stuff + - preferences - almost ready to start + - anything moderation-related - not started -```elixir -def deps do - [ - {:hipdster, "~> 0.1.0"} - ] -end -``` -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at . diff --git a/config/config.exs b/config/config.exs index 38fb94d..9f08465 100644 --- a/config/config.exs +++ b/config/config.exs @@ -4,8 +4,27 @@ import Config #  Application.get_env(:hipdster, :plc_server) config :hipdster, - plc_server: "plc.bsky-sandbox.dev", - appview_server: "api.bsky-sandbox.dev", - relay_server: "bgs.bsky-sandbox.dev", - pds_host: "abyss.computer", # ignore pls for now - multicodec_csv_path: "multicodec.csv" + plc_server: "plc.directory", + appview_server: "public.api.bsky.app", + relay_server: "bsky.network", + # ignore pls for now + pds_host: "abyss.computer", + multicodec_csv_path: "multicodec.csv", + admin_password: "admin", + # or Ecto.Adapters.Postgres in production + ecto_adapter: Ecto.Adapters.SQLite3, + ecto_repos: [Hipdster.Database], + port: (case Mix.env do + :prod -> 3999 + :dev -> 4000 + :test -> 4001 + end), + # Example HS256 secret for access and refresh JWTs + jwt_key: <<16474290805911645537423060771945528686550823130298449174717469148262408363010::256>> + +config :hipdster, Hipdster.Database, + # Replace with Postgres URL in production! + url: "sqlite3:///pds" + +config :mnesia, + dir: ~c".mnesia/#{Mix.env()}/#{node()}" diff --git a/lib/hipdster/auth.ex b/lib/hipdster/auth.ex new file mode 100644 index 0000000..eade3e2 --- /dev/null +++ b/lib/hipdster/auth.ex @@ -0,0 +1,36 @@ +defmodule Hipdster.Auth do + @moduledoc """ + Authentication and session management + """ +alias Hipdster.User + + def generate_session(_, username, pw) do + if Hipdster.User.authenticate(username, pw) do + Hipdster.User.get(username) + |> generate_session() + else + %{ + error: "AuthenticationRequired", + message: "Invalid identifier or password" + } + end + end + + def generate_session(%User{handle: handle, did: did} = u) do + %{ + accessJwt: Hipdster.Auth.JWT.access_jwt(u, "main"), + refreshJwt: Hipdster.Auth.JWT.refresh_jwt(u, "main"), + handle: handle, + did: did + } + end + + def admin_auth("Basic " <> credentials) do + with {:ok, creds} <- Base.decode64(credentials), + ["admin", password] <- String.split(creds, ":") do + Application.get_env(:hipdster, :admin_password) == password + else + _ -> false + end + end +end diff --git a/lib/hipdster/auth/context.ex b/lib/hipdster/auth/context.ex new file mode 100644 index 0000000..98b0f17 --- /dev/null +++ b/lib/hipdster/auth/context.ex @@ -0,0 +1,76 @@ +defmodule Hipdster.Auth.Context do + @moduledoc """ + A struct containing some context for a request, + such as the currently logged in user, the app password + used, whether it's a refresh or access token, and + probably more + """ + + defstruct [ + :authed, + :user, + :is_app_pwd?, + :app_password_name, + :token_type + ] + + @type token_type() :: :access | :refresh + + @type t :: %__MODULE__{ + authed: boolean(), + user: Hipdster.User.t() | nil, + is_app_pwd?: boolean(), + app_password_name: String.t() | nil, + token_type: token_type() | nil + } + + def authed?(%__MODULE__{authed: authed}), do: authed + + def app_pwd?(%__MODULE__{is_app_pwd?: is_app_pwd?}), do: is_app_pwd? + + def parse_jwt(jwt, hs256_secret \\ Application.get_env(:hipdster, :jwt_key)) do + Hipdster.Auth.Session.verify(jwt, hs256_secret) + |> json_to_ctx() + end + + def unauthed(), do: %__MODULE__{ + authed: false, + user: nil, + is_app_pwd?: false, + app_password_name: nil, + token_type: nil + } + + defp json_to_ctx({:error, _}), + do: unauthed() + + defp json_to_ctx(%{ + "iss" => did, + "sub" => %{"pwd" => app_password_name, "scope" => scope} + }) do + %{is_app_pwd?: is_app_pwd, app_password_name: app_password_name} = + app_password(app_password_name) + + %__MODULE__{ + authed: true, + user: Hipdster.User.get(did), + is_app_pwd?: is_app_pwd, + app_password_name: app_password_name, + token_type: token_type(scope) + } + end + + defp app_password("main"), do: %{is_app_pwd?: false, app_password_name: nil} + + defp app_password(<<>> <> app_password_name), + do: %{is_app_pwd?: true, app_password_name: app_password_name} + + defp token_type("com.atproto.access"), do: :access + defp token_type("com.atproto.refresh"), do: :refresh + + + def parse_header(nil), do: unauthed() + def parse_header("Bearer " <> jwt), do: parse_jwt(jwt) + def parse_header(_), do: unauthed() + +end diff --git a/lib/hipdster/auth/jwt.ex b/lib/hipdster/auth/jwt.ex new file mode 100644 index 0000000..3963fe5 --- /dev/null +++ b/lib/hipdster/auth/jwt.ex @@ -0,0 +1,88 @@ +defmodule Hipdster.Auth.JWT do + @moduledoc """ + JWT generation and verification + """ + defmodule Internal do + @moduledoc false + use Rustler, otp_app: :hipdster, crate: "hipdster_auth_jwt_internal" + + import Hipdster.Helpers + + def generate_k256_jwt(_account_did, _service_did, _subject, _key), + do: :erlang.nif_error(:nif_not_loaded) + + def generate_hs256_jwt(_account_did, _subject, _hs256_key, _time_in_minutes), + do: :erlang.nif_error(:nif_not_loaded) + + def!(generate_hs256_jwt(account_did, subject, hs256_key, time_in_minutes)) + + def verify_hs256_jwt(_jwt, _key), do: :erlang.nif_error(:nif_not_loaded) + end + + import Hipdster.Helpers + + @spec interservice(Hipdster.User.t(), Hipdster.Identity.did(), String.t()) :: + {:ok, binary()} | {:error, String.t()} + def interservice( + %Hipdster.User{ + signing_key: %Hipdster.K256.PrivateKey{ + privkey: <<>> <> signing_key_bytes + }, + did: user_did + }, + service_did, + subject \\ "" + ) do + Hipdster.Auth.JWT.Internal.generate_k256_jwt( + user_did, + service_did, + subject, + signing_key_bytes + ) + end + + def!(interservice(user, service_did, subject)) + def!(interservice(user, service_did)) + + def access_jwt( + %Hipdster.User{did: did}, + app_password_name \\ "main", + hs256_secret \\ Application.get_env(:hipdster, :jwt_key) + ) do + Hipdster.Auth.JWT.Internal.generate_hs256_jwt!( + did, + Jason.encode!(%{scope: "com.atproto.access", pwd: app_password_name}), + hs256_secret, + 60 + ) + end + + def refresh_jwt( + %Hipdster.User{did: did}, + app_password_name \\ "main", + hs256_secret \\ Application.get_env(:hipdster, :jwt_key) + ) do + Hipdster.Auth.JWT.Internal.generate_hs256_jwt!( + did, + Jason.encode!(%{scope: "com.atproto.refresh", pwd: app_password_name}), + hs256_secret, + 130_000 + ) + end + + def verify(jwt, hs256_secret \\ Application.get_env(:hipdster, :jwt_key)) do + Hipdster.Auth.JWT.Internal.verify_hs256_jwt(jwt, hs256_secret) + |> case do + {:ok, json} -> + Jason.decode!(json) + |> then(fn %{"sub" => sub} = tok -> put_in(tok["sub"], Jason.decode!(sub)) end) + + {:error, _reason} = e -> + e + end + end + + def is_valid_pwd?("main"), do: true + def is_valid_pwd?(_), do: false + +end diff --git a/lib/hipdster/auth/session.ex b/lib/hipdster/auth/session.ex new file mode 100644 index 0000000..448e87d --- /dev/null +++ b/lib/hipdster/auth/session.ex @@ -0,0 +1,92 @@ +defmodule Hipdster.Auth.Session do + require Matcha + + use Memento.Table, + attributes: [:refresh_jwt, :access_jwt, :did], + index: [:access_jwt, :did], + type: :set + + @type t :: %__MODULE__{ + refresh_jwt: String.t(), + access_jwt: String.t(), + did: Hipdster.Identity.did() + } + + def new(uname) do + %{did: did, refreshJwt: r_jwt, accessJwt: a_jwt} = + resp = + Hipdster.User.get(uname) + |> Hipdster.Auth.generate_session() + + sess = %__MODULE__{refresh_jwt: r_jwt, access_jwt: a_jwt, did: did} + + Memento.transaction!(fn -> + Memento.Query.write(sess) + end) + + resp + end + + def new(uname, pw) do + if Hipdster.User.authenticate(uname, pw) do + new(uname) + end + end + + def find_r_jwt(jwt) do + Memento.transaction!(fn -> + Memento.Query.select_raw( + __MODULE__, + (Matcha.spec do + {__MODULE__, ^jwt, _, _} = ses -> ses + end).source + ) + end) + |> List.first() + end + + def find_a_jwt(jwt) do + Memento.transaction!(fn -> + Memento.Query.select_raw( + __MODULE__, + (Matcha.spec do + {__MODULE__, _, ^jwt, _} = ses -> ses + end).source + ) + end) + |> List.first() + end + + def delete(jwt) do + if find_r_jwt(jwt) do + Memento.transaction!(fn -> + Memento.Query.delete(__MODULE__, jwt) + end) + else + :error + end + end + + def refresh(r_jwt) do + case find_r_jwt(r_jwt) do + nil -> {:error, "Invalid token"} + %{did: did} -> delete(r_jwt) + new(did) + end + end + + def verify(jwt, hs256 \\ Application.get_env(:hipdster, :jwt_key)) do + Hipdster.Auth.JWT.verify(jwt, hs256) + |> case do + {:error, reason} -> + {:error, reason} + + json -> + case json["scope"] do + "com.atproto.access" -> if find_r_jwt(jwt), do: json, else: {:error, "Invalid token"} + "com.atproto.refresh" -> if find_a_jwt(jwt), do: json, else: {:error, "Invalid token"} + _ -> {:error, "Invalid scope"} + end + end + end +end diff --git a/lib/hipdster/auth/session/cleaner.ex b/lib/hipdster/auth/session/cleaner.ex new file mode 100644 index 0000000..81518aa --- /dev/null +++ b/lib/hipdster/auth/session/cleaner.ex @@ -0,0 +1,33 @@ +defmodule Hipdster.Auth.Session.Cleaner do + @moduledoc """ + Deletes expired sessions on the hour + """ + + use GenServer + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @impl GenServer + def init(_) do + :timer.send_interval(3_600_000, :clean) + {:ok, nil} + end + + @impl GenServer + def handle_info(:clean, state) do + Memento.transaction!(fn -> + :mnesia.foldl( + fn {_, r_jwt, _, _}, _ -> + unless Hipdster.Auth.JWT.verify(r_jwt) do + Hipdster.Auth.Session.delete(r_jwt) + end + end, + nil, + Hipdster.Auth.Session + ) + end) + {:noreply, state} + end +end diff --git a/lib/hipdster/blob.ex b/lib/hipdster/blob.ex new file mode 100644 index 0000000..38ec8c4 --- /dev/null +++ b/lib/hipdster/blob.ex @@ -0,0 +1,142 @@ +defmodule Hipdster.Blob do + @moduledoc """ + An ATProto blob. Has a CID, mime-type, raw data, DID of the owner. + A list of references will likely be added once we have repos working. + """ + alias Hipdster.CID + + use Ecto.Schema + import Ecto.Query + + schema "blobs" do + # hash of did + cid (sha256) to avoid duplicates + field(:hash, :binary) + field(:did, :string) + field(:cid, Ecto.Types.Cid) + field(:mime_type, :string) + field(:data, :binary) + timestamps() + end + + @type t :: %__MODULE__{ + id: integer(), + cid: Hipdster.CID.t(), + mime_type: String.t(), + data: binary(), + did: Hipdster.Identity.did(), + hash: <<_::256>>, + inserted_at: NaiveDateTime.t(), + updated_at: NaiveDateTime.t(), + } + + @doc """ + Creates a new blob from raw bytes and the DID of the owner. + Calculates the CID, mime-type, and hash. + """ + def new(raw_bytes, %Hipdster.User{did: did}) do + %__MODULE__{ + cid: + raw_bytes + |> bytes_to_cid(), + mime_type: get_mime_type(raw_bytes), + data: raw_bytes, + did: did, + hash: hash(did, raw_bytes) + } + end + + @doc """ + CID reference to blob, with multicodec type `raw`, + in accordance with ATProto data model. + """ + @spec bytes_to_cid(binary()) :: Hipdster.CID.t() + def bytes_to_cid(bytes) do + with {:ok, multihash} <- Multihash.encode(:sha2_256, :crypto.hash(:sha256, bytes)) do + multihash |> CID.cid!("raw") + end + end + + @doc """ + Given a DID and a CID, appends the DID to the CID as a string + and takes the sha256 hash. This is used to ensure uniqueness + of blobs in the database, even when two users upload the + exact same blob. That way one user's blob is not overwritten + by another user's. This is also used to lookup blobs fast, + though SQL is fast enough that it doesn't really matter. + + (This is not part of the ATP spec, just a weird + hack I added) + """ + def hash(did, "bafkr" <> _ = cid) do + hash(did, CID.decode_cid!(cid)) + end + def hash(did, %CID{} = cid), do: :crypto.hash(:sha256, did <> cid_string(cid)) + def hash(did, bytes) do + hash(did, bytes |> bytes_to_cid()) + end + + + @spec get_mime_type(binary()) :: String.t() + @spec get_mime_type(nil) :: String.t() + @spec get_mime_type(Infer.Type.t()) :: String.t() + @doc """ + Given the bytes of the blob being uploaded, returns the mime-type. + If the mime-type cannot be determined, returns "application/octet-stream". + """ + def get_mime_type(<<>> <> data) do + Infer.get(data) + |> get_mime_type() + end + def get_mime_type(%Infer.Type{mime_type: mime_type}), do: mime_type + def get_mime_type(nil), do: "application/octet-stream" + + @spec cid_string(Hipdster.Blob.t() | Hipdster.CID.t() | String.t()) :: binary() + @doc """ + Given a blob or CID, returns the string representation of the CID. + """ + def cid_string(%__MODULE__{cid: cid}), do: cid_string(cid) + def cid_string(%CID{} = cid), do: to_string(cid) + def cid_string("bafkr" <> _ = cid), do: cid + + @doc """ + Given a blob, saves it to the database. + """ + def save(%__MODULE__{} = blob), do: Hipdster.Database.insert(blob) + + @doc """ + Given the DID of the owner of a blob, + and the CID of the blob itself, this + returns the blob itself in the database. + Uses the hash to ensure uniqueness. + """ + @spec get(Hipdster.CID.t() | String.t(), String.t()) :: t() + def get(cid, "did:" <> _ = did) do + hash = hash(did, cid_string(cid)) + Hipdster.Blob + |> where(hash: ^hash) + |> Hipdster.Database.one() + end + + def get("did:" <> _ = did, cid), do: get(cid, did) + + @doc """ + Given a CID, returns all blobs that share that CID. + (These blobs may be owned by different users, but + should be identical otherwise). + """ + @spec with_cid(Hipdster.CID.t()) :: [t()] + def with_cid(cid) do + Hipdster.Blob + |> where(cid: ^cid) + |> Hipdster.Database.all() + end + + @doc """ + All blobs owned by the given user. + """ + @spec of_user(Hipdster.User.t()) :: [t()] + def of_user(%Hipdster.User{did: did}) do + (from Hipdster.Blob, where: [did: ^did]) + |> Hipdster.Database.all() + end +end diff --git a/lib/hipdster/cid.ex b/lib/hipdster/cid.ex index dda0f24..8ecf11d 100644 --- a/lib/hipdster/cid.ex +++ b/lib/hipdster/cid.ex @@ -665,7 +665,6 @@ defmodule Hipdster.CID do {:error, :unsupported_conversion} """ - @spec to_version(t(), cid_version()) :: t() def to_version(cid, destination_version) when is_map(cid) and is_integer(destination_version) and destination_version <= @current_version do convert_version(cid, destination_version) end @@ -758,4 +757,7 @@ defmodule Hipdster.CID do end end + defimpl String.Chars, for: CID do + def to_string(cid), do: CID.encode!(cid, :base32_lower) + end end diff --git a/lib/hipdster/dagcbor.ex b/lib/hipdster/dagcbor.ex index 20e6921..1096630 100644 --- a/lib/hipdster/dagcbor.ex +++ b/lib/hipdster/dagcbor.ex @@ -24,6 +24,7 @@ defmodule Hipdster.DagCBOR do {:ok, to_string(cbor)} end end + def encode(%{} = json) do with {:ok, json} <- Jason.encode(json), do: encode(json) end @@ -32,11 +33,22 @@ defmodule Hipdster.DagCBOR do with {:ok, json} <- Jason.encode(l), do: encode(json) end + @doc """ + Decodes a CBOR binary into a JSON string. + """ @spec decode_json(binary()) :: {:error, binary()} | {:ok, String.t()} def decode_json(cbor) do Internal.decode_dag_cbor(cbor) end + @doc """ + Decodes a CBOR binary into an erlang term. + Examples: + + iex> Hipdster.DagCBOR.decode(<<131,0,3,4>>) + ...> |> elem(1) + [0, 3, 4] + """ @spec decode(binary()) :: {:error, binary() | Jason.DecodeError.t()} | {:ok, any()} def decode (cbor) do with {:ok, json} <- decode_json(cbor), do: Jason.decode(json) diff --git a/lib/hipdster/db/cid.ex b/lib/hipdster/db/cid.ex new file mode 100644 index 0000000..df91025 --- /dev/null +++ b/lib/hipdster/db/cid.ex @@ -0,0 +1,20 @@ +defmodule Ecto.Types.Cid do + alias Hipdster.CID + use Ecto.Type + + def type, do: :binary + def cast(:any, term), do: {:ok, term} + def cast(term), do: {:ok, term} + + def load(term) when is_binary(term) do + {:ok, + term + |> CID.decode_cid!()} + end + + def dump(term), + do: + {:ok, + term + |> CID.encode!(:base32_lower)} +end diff --git a/lib/hipdster/db/database.ex b/lib/hipdster/db/database.ex new file mode 100644 index 0000000..9335da1 --- /dev/null +++ b/lib/hipdster/db/database.ex @@ -0,0 +1,11 @@ +defmodule Hipdster.Database do + @moduledoc """ + This is an Ecto Repo! + The term `Repo` is not used here + to avoid confusion with ATProto Repos. + """ + + use Ecto.Repo, + otp_app: :hipdster, + adapter: Application.compile_env(:hipdster, :ecto_adapter) +end diff --git a/lib/hipdster/db/erlterm.ex b/lib/hipdster/db/erlterm.ex new file mode 100644 index 0000000..561135e --- /dev/null +++ b/lib/hipdster/db/erlterm.ex @@ -0,0 +1,38 @@ +defmodule Ecto.Type.ErlangTerm do + @moduledoc """ + A custom Ecto type for handling the serialization of arbitrary + data types stored as binary data in the database. Requires the + underlying DB field to be a binary. + Taken from https://fly.io/phoenix-files/exploring-options-for-storing-custom-data-in-ecto/ + """ + use Ecto.Type + def type, do: :binary + + @doc """ + Provides custom casting rules for params. Nothing changes here. + We only need to handle deserialization. + """ + def cast(:any, term), do: {:ok, term} + def cast(term), do: {:ok, term} + + @doc """ + Convert the raw binary value from the database back to + the desired term. + """ + def load(raw_binary) when is_binary(raw_binary), + do: + {:ok, + raw_binary + |> :zlib.gunzip() + |> :erlang.binary_to_term()} + + @doc """ + Converting the data structure to binary for storage. + """ + def dump(term), + do: + {:ok, + term + |> :erlang.term_to_binary() + |> :zlib.gzip()} +end diff --git a/lib/hipdster/db/mnesia.ex b/lib/hipdster/db/mnesia.ex new file mode 100644 index 0000000..e6fc24b --- /dev/null +++ b/lib/hipdster/db/mnesia.ex @@ -0,0 +1,16 @@ +defmodule Hipdster.Database.Mnesia do + # One can never have too many databases + + @moduledoc """ + Right now Mnesia is just used to maintain + a cache of session jwts and their validity. + """ + @tables [Hipdster.Auth.Session] + + def tables, do: @tables + + def create_tables do + tables() + |> Enum.map(&Memento.Table.create/1) + end +end diff --git a/lib/hipdster/db/user.ex b/lib/hipdster/db/user.ex new file mode 100644 index 0000000..7f67807 --- /dev/null +++ b/lib/hipdster/db/user.ex @@ -0,0 +1,72 @@ +defmodule Hipdster.User do + @moduledoc """ + A user in the database + """ + alias Hipdster.K256 + + use Ecto.Schema + import Ecto.Query + + schema "users" do + field(:did, :string) + field(:handle, :string) + field(:password_hash, :string) + field(:signing_key, Ecto.Type.ErlangTerm) + field(:rotation_key, Ecto.Type.ErlangTerm) + field(:data, :map) + end + + @type key :: Hipdster.K256.PrivateKey.t() | Hipdster.K256.PublicKey.t() + + @type signing_key :: key() + @type rotation_key :: key() + + @type t :: %__MODULE__{ + did: Hipdster.Identity.did(), + handle: String.t(), + password_hash: String.t(), + signing_key: signing_key(), + rotation_key: rotation_key() | [rotation_key()], + data: map() + } + + def get("did:" <> _ = did) do + from(u in __MODULE__, + where: u.did == ^did, + select: u + ) + |> Hipdster.Database.one() + end + + def get(handle) do + from(u in __MODULE__, + where: u.handle == ^handle, + select: u + ) + |> Hipdster.Database.one() + end + + def authenticate(username, pw) do + with %__MODULE__{password_hash: hash} = user <- get(username), + true <- Argon2.verify_pass(pw, hash) do + user + else + _ -> false + end + end + + def create(handle, pw) do + %{did: did, signing_key: signing_key, rotation_key: rotation_key} = + Hipdster.DidGenerator.generate_did(handle) + + %__MODULE__{ + did: did, + handle: handle, + password_hash: Argon2.hash_pwd_salt(pw), + signing_key: signing_key |> K256.PrivateKey.from_hex(), + rotation_key: rotation_key |> K256.PrivateKey.from_hex(), + data: %{"preferences" => %{}} + } + |> tap(&Hipdster.Database.insert/1) + end +end diff --git a/lib/hipdster/db/user/preferences.ex b/lib/hipdster/db/user/preferences.ex new file mode 100644 index 0000000..a32fb1e --- /dev/null +++ b/lib/hipdster/db/user/preferences.ex @@ -0,0 +1,13 @@ +defmodule Hipdster.User.Preferences do + @moduledoc """ + A user's preferences + """ + + import Ecto.Changeset + + def put(%Hipdster.User{} = user, %{} = params) do + user + |> change(%{data: %{"preferences" => params}}) + |> Hipdster.Database.update() + end +end diff --git a/lib/hipdster/didgenerator.ex b/lib/hipdster/didgenerator.ex index 79f1d4e..e526cde 100644 --- a/lib/hipdster/didgenerator.ex +++ b/lib/hipdster/didgenerator.ex @@ -18,24 +18,6 @@ defmodule Hipdster.DidGenerator do |> String.downcase() end - @spec publish_to_plc(map(), String.t()) :: - {:error, - %{ - :__exception__ => true, - :__struct__ => Jason.EncodeError | Protocol.UndefinedError, - optional(atom()) => any() - }} - | %{ - :__struct__ => - HTTPoison.AsyncResponse | HTTPoison.MaybeRedirect | HTTPoison.Response, - optional(:body) => any(), - optional(:headers) => list(), - optional(:id) => reference(), - optional(:redirect_url) => any(), - optional(:request) => HTTPoison.Request.t(), - optional(:request_url) => any(), - optional(:status_code) => integer() - } def publish_to_plc( %{"type" => "plc_operation", "prev" => nil} = signed_genesis, "https://" <> plc_url diff --git a/lib/hipdster/identity.ex b/lib/hipdster/identity.ex new file mode 100644 index 0000000..968fe4c --- /dev/null +++ b/lib/hipdster/identity.ex @@ -0,0 +1,73 @@ +defmodule Hipdster.Identity do + @moduledoc """ + Stuff like resolving a handle, fetching a DID, generating JWTs, etc + """ + + import Hipdster.Helpers + + @typedoc """ + A string representing a DID in the format `did:plc:` + """ + @type did_plc :: <<_::256>> + @typedoc """ + A string representing a DID in the format `did:web:` + """ + @type did_web :: String.t() + + @typedoc """ + A DID as a string + """ + @type did :: did_plc() | did_web() + + @typedoc """ + A handle as a string + """ + @type handle :: String.t() + + @spec resolve_handle(String.t()) :: {:ok, did()} | {:error, any()} + def resolve_handle(domain) do + lookup_did_by_dns(domain) + |> case do + {:ok, did} -> + is_did?({:ok, did}) + + {:error, dns_error} -> + get_did_from_http(domain) + |> case do + {:ok, did} -> is_did?({:ok, did}) + {:error, http_error} -> {:error, dns_error: dns_error, http_error: http_error} + end + end + end + + @spec is_did?({:ok, did()}) :: {:ok, did()} + def is_did?({:ok, "did:" <> _did} = arg), do: arg + @spec is_did?({:ok, any()}) :: {:error, not_a_did: any()} + def is_did?({:ok, anything_else}), do: {:error, not_a_did: anything_else} + + defp lookup_did_by_dns(domain) do + handle_errors do + [[did_record]] = :inet_res.lookup(~c"_atproto.#{domain}", :in, :txt) + "did=" <> did = to_string(did_record) + did + end + end + + defp get_did_from_http(domain) do + handle_errors do + HTTPoison.get!("https://#{domain}/.well-known/atproto-did").body + end + end + + def did_url("did:plc:" <> _did = did), + do: "https://#{Application.get_env(:hipdster, :plc_server)}/#{did}" + + def did_url("did:web:" <> domain), do: "https://#{domain}/.well-known/did.json" + + def get_did(did) do + with {:ok, %{body: body}} <- + HTTPoison.get(did_url(did)) do + {:ok, Jason.decode!(body)} + end + end +end diff --git a/lib/hipdster/k256.ex b/lib/hipdster/k256.ex index 9aed6e4..38b2ea7 100644 --- a/lib/hipdster/k256.ex +++ b/lib/hipdster/k256.ex @@ -18,7 +18,6 @@ defmodule Hipdster.K256 do def create(sig), do: %__MODULE__{sig: sig} def bytes(%__MODULE__{sig: sig}), do: sig - # TODO: make this unnecessary def bytes({:error, e}), do: raise(e) @spec verify(t(), Hipdster.K256.PublicKey.t(), binary()) :: @@ -28,7 +27,10 @@ defmodule Hipdster.K256 do %{pubkey: pubkey, __struct__: Hipdster.K256.PublicKey}, message ) do - case Hipdster.K256.Internal.verify_signature(pubkey, message, sig) do + pubkey + |> Base.encode16() + |> Hipdster.K256.Internal.verify_signature(message, sig) + |> case do true -> {:ok, "Signature #{inspect(sig)} verified"} false -> {:error, "Signature #{inspect(sig)} could not be verified"} tuple -> tuple @@ -110,7 +112,7 @@ defmodule Hipdster.K256 do @spec create(PrivateKey.t()) :: t() def create(%PrivateKey{privkey: privkey}) do case Hipdster.K256.Internal.create_public_key(privkey) do - {:ok, pubkey} -> from_binary(pubkey) + {:ok, pubkey} -> from_hex(pubkey) {:error, e} -> raise e end end @@ -127,7 +129,9 @@ defmodule Hipdster.K256 do Wrapper around the `k256` crate's `k256::PublicKey::compress` function. """ def compress(%__MODULE__{pubkey: pubkey}) do - case Hipdster.K256.Internal.compress_public_key(pubkey) do + Base.encode16(pubkey, case: :lower) + |> Hipdster.K256.Internal.compress_public_key() + |> case do {:ok, compressed} -> compressed {:error, e} -> raise e end diff --git a/lib/hipdster/macros.ex b/lib/hipdster/macros.ex new file mode 100644 index 0000000..04c0a4d --- /dev/null +++ b/lib/hipdster/macros.ex @@ -0,0 +1,28 @@ +defmodule Hipdster.Helpers do + @moduledoc """ + Common macros & functions that make Elixir easier + """ + # Gonna move some common stuff here soon + + defmacro def!({name, _, args}) do + quote do + def (unquote {:"#{name}!", [], args}) do + case unquote({name, [], args}) do + {:ok, value} -> value + {:error, error} -> raise error + end + end + end + end + + defmacro handle_errors(do: block) do + quote do + try do + {:ok, unquote(block)} + rescue + error -> {:error, error} + end + end + end + +end diff --git a/lib/hipdster/multicodec.ex b/lib/hipdster/multicodec.ex index 1c72c4a..2c0addc 100644 --- a/lib/hipdster/multicodec.ex +++ b/lib/hipdster/multicodec.ex @@ -1,6 +1,8 @@ defmodule Hipdster.Multicodec do use GenServer + alias Matcha.Table.ETS + require ETS @moduledoc """ # Multicodec [Multicodec](https://github.com/multiformats/multicodec) is one of the Multiformats used in IPFS and, @@ -27,7 +29,7 @@ defmodule Hipdster.Multicodec do @type multi_codec() :: any() - def start_link(multicodec_csv \\ Application.get_env(:hipdster, :multicodec_csv_path)) do + def start_link(multicodec_csv) do GenServer.start_link(__MODULE__, multicodec_csv, name: __MODULE__) end @@ -36,12 +38,9 @@ defmodule Hipdster.Multicodec do {:ok, csv_path |> File.stream!() - |> read_csv()} - end - - @impl GenServer - def handle_call(:multicodec_map, _from, state) do - {:reply, state, state} + |> read_csv() + |> load_into_ets() + } end @doc """ @@ -56,23 +55,31 @@ defmodule Hipdster.Multicodec do end) end - @spec multicodec_map() :: map() - @doc """ - Returns a map of multicodec names to integer codes. - """ - def multicodec_map() do - GenServer.call(__MODULE__, :multicodec_map) + defp load_into_ets(multicodec_map) do + tab = :ets.new(__MODULE__, [:set, :public, :named_table]) + for {codec, code} <- multicodec_map do + :ets.insert(__MODULE__, {codec, code}) + end + tab end - @spec bytes_to_codec() :: map() + + @spec bytes_to_codec(integer()) :: atom() | [] @doc """ - Returns a map of integer codes to multicodec names. - Basically the opposite of `multicodec_map()`. + For the given integer code, returns the codec name. """ - def bytes_to_codec() do - for {codec, bytes} <- multicodec_map(), into: %{} do - {bytes, codec} + def bytes_to_codec(bytes) do + ETS.select __MODULE__ do + {codec, code} when code == bytes -> codec + end + |> List.first + end + + def codec_to_bytes(codec) do + ETS.select __MODULE__ do + {codec, code} when codec == codec -> code end + |> List.first end @doc """ @@ -81,9 +88,7 @@ defmodule Hipdster.Multicodec do """ def encode!(bytes, "" <> codec) do <> end @@ -112,7 +117,7 @@ defmodule Hipdster.Multicodec do def codec_decode("" <> encoded) do try do with {prefix, rest} <- Varint.LEB128.decode(<>), - codec <- bytes_to_codec()[prefix], + codec when is_atom(codec) <- bytes_to_codec(prefix), do: {:ok, {rest, to_string(codec)}} catch _, e -> {:error, e} @@ -124,7 +129,7 @@ defmodule Hipdster.Multicodec do """ @spec codec?(binary()) :: boolean() def codec?("" <> codec) do - Map.has_key?(multicodec_map(), String.to_atom(codec)) + :ets.member(__MODULE__, String.to_atom(codec)) end @doc """ @@ -132,8 +137,8 @@ defmodule Hipdster.Multicodec do """ @spec codecs() :: [String.t()] def codecs do - multicodec_map() - |> Map.keys() + :ets.tab2list(__MODULE__) + |> Stream.map(&elem(&1, 0)) |> Enum.map(&to_string/1) end end diff --git a/lib/hipdster/service/http.ex b/lib/hipdster/service/http.ex new file mode 100644 index 0000000..d643774 --- /dev/null +++ b/lib/hipdster/service/http.ex @@ -0,0 +1,307 @@ +defmodule Hipdster.Http do + @moduledoc """ + The XRPC interface to the PDS, including AppView proxying + """ + alias Hipdster.XRPC + require XRPC + + use Plug.Router + + plug(:match) + plug(:dispatch) + + plug(Plug.Parsers, + parsers: [:json], + pass: ["text/*"], + json_decoder: Jason + ) + + options "/xrpc/:any" do + conn + |> put_resp_header("access-control-allow-origin", "*") + |> put_resp_header("access-control-allow-methods", "GET,HEAD,PUT,PATCH,POST,DELETE") + |> put_resp_header("access-control-allow-headers", "atproto-accept-labelers,authorization,content-type") + |> put_resp_header("access-control-max-age", "86400") + |> put_resp_header("content-length", "0") + |> send_resp(204, "") + end + + get "/" do + send_resp(conn, 200, """ + Hello from Hipdster + ATProto PDS + routes /xrpc/* + + Code on GitHub page + find it at ovnanova + slash hexpds + """) + end + + get "/favicon.ico" do + send_resp(conn, 200, "Why would a PDS need a favicon?") + end + + get "/.well-known/atproto-did" do + {status, resp} = + case Hipdster.User.get(conn.host) do + %Hipdster.User{did: did} -> {200, did} + _ -> {404, "User not found"} + end + + send_resp(conn, status, resp) + end + + get "/xrpc/:method" do + conn = fetch_query_params(conn) + + # If you're using a non-known query param you deserve that exception, hence String.to_existing_atom/1 + # Ignore above comment. Have to use to_atom/1 because of appview proxying which involves inherently unknown routes + params = + for {key, val} <- conn.query_params, into: %{}, do: {String.to_atom(key), val} + + context = get_context(conn) + + {statuscode, json_body} = + try do + # We can handle the method + IO.puts("Got query: #{method} #{inspect(params)}") + + xrpc_query(conn, method, params, context) + |> IO.inspect() + catch + _, e_from_method -> + try do + # We can't handle the method - try the appview + case e_from_method do + %FunctionClauseError{} -> IO.inspect(e_from_method) + :function_clause -> IO.inspect(e_from_method) + _ -> throw(e_from_method) + end + + forward_query_to_appview(IO.inspect(appview_for(conn)), conn, method, params, context) + catch + _, e -> + IO.inspect(e, label: "AppView proxying error") + + {500, + %{ + error: "Error", + message: "Oh no! Bad request or internal server error", + debug: inspect(e) + }} + end + end + + case json_body do + {:blob, blob} -> + conn + |> Plug.Conn.put_resp_content_type(blob.mime_type) + |> Plug.Conn.send_resp(200, blob.data) + |> Plug.Conn.halt() + |> IO.inspect() + + _ -> + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.put_resp_header("access-control-allow-origin", "*") + |> Plug.Conn.send_resp(statuscode, Jason.encode!(json_body)) + end + end + + post "/xrpc/:method" do + {:ok, body, _} = Plug.Conn.read_body(conn) + + body = + for {key, val} <- Jason.decode!(body), into: %{}, do: {String.to_atom(key), val} + + {statuscode, json_resp} = xrpc_procedure(conn, method, body, get_context(conn)) + + case json_resp do + {:blob, blob} -> + conn + |> Plug.Conn.put_resp_content_type(blob.mime_type) + |> Plug.Conn.send_resp(200, blob.data) + + _ -> + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.put_resp_header("access-control-allow-origin", "*") + |> Plug.Conn.send_resp(statuscode, Jason.encode!(json_resp)) + end + end + + defp appview_for(%Plug.Conn{req_headers: r_h}) do + r_h + |> Enum.into(%{}) + |> Map.get("atproto-proxy") + end + + def url_of(nil), do: nil + + # Caching wouldn't be a bad idea + def url_of(atproto_proxy) do + with [did, service] <- String.split(atproto_proxy, "#"), + label <- "##{service}", + {:ok, did_doc} <- Hipdster.Identity.get_did(did), + %{"service" => services} <- did_doc, + %{"serviceEndpoint" => "https://" <> endpoint} <- + services + |> Enum.find(fn + %{"id" => ^label} -> true + _ -> false + end) do + endpoint + else + err -> raise "Bad atproto-proxy header: #{inspect(err)}" + end + end + + defp forward_query_to_appview(appview_did, _conn, method, params, context) do + # Ignore auth for now + + headers = + case context do + %{authed: true, user: %{did: did}} -> + [ + Authorization: + "Bearer " <> + Hipdster.Auth.JWT.interservice!(did, appview_did) + ] + + _ -> + [] + end + + %{status_code: statuscode, body: json_body} = + ("https://" <> + (url_of(appview_did) || Application.get_env(:hipdster, :appview_server)) <> + "/xrpc/" <> method <> "?" <> URI.encode_query(params)) + |> HTTPoison.get!(headers) + + {statuscode, Jason.decode!(json_body)} + end + + defp get_context(%Plug.Conn{req_headers: r_h}) do + r_h + |> Enum.into(%{}) + |> Map.get("authorization") + |> Hipdster.Auth.Context.parse_header() + end + + @spec xrpc_query(Plug.Conn.t(), String.t(), map(), Hipdster.Auth.Context.t()) :: + {integer(), map() | {:blob, Hipdster.Blob.t()}} + + XRPC.query _, "app.bsky.actor.getPreferences", %{}, ctx do + case ctx do + %{user: %Hipdster.User{} = user, token_type: :access} -> + {200, %{preferences: user.data["preferences"]}} + + _ -> + {401, %{error: "Unauthorized", message: "Not authorized"}} + end + end + + XRPC.query _, "com.atproto.sync.getBlob", %{did: did, cid: cid}, _ do + with %Hipdster.Blob{} = blob <- Hipdster.Blob.get(cid, did) do + {200, {:blob, blob}} + else + _ -> {400, %{error: "InvalidRequest", message: "No such blob"}} + end + end + + XRPC.query _, "com.atproto.sync.listBlobs", opts, _ do + case Hipdster.Xrpc.Query.ListBlobs.list_blobs( + opts[:did], + opts[:since], + String.to_integer(opts[:limit] || 500), + Hipdster.CID.decode_cid!(opts[:cursor]) + ) do + %{cids: cids, cursor: next_cursor} -> + {200, %{cursor: next_cursor, cids: Enum.map(cids, &to_string/1)}} + + other -> + {400, %{error: "InvalidRequest", message: inspect(other)}} + end + end + + XRPC.query _, "com.atproto.server.getSession", %{}, ctx do + case ctx do + %{user: %Hipdster.User{did: did, handle: handle}, token_type: :access} -> + {200, %{handle: handle, did: did}} + + _ -> + {401, %{error: "Unauthorized", message: "Not authorized"}} + end + end + + XRPC.query _, "com.atproto.server.describeServer", _, _ do + IO.puts("Describing server...") + + {200, + %{ + # These will all change, obviously + availableUserDomains: ["localhost"], + did: "did:web:localhost" + }} + end + + @spec xrpc_procedure(Plug.Conn.t(), String.t(), map(), Hipdster.Auth.Context.t()) :: + {integer(), map()} + + XRPC.procedure _, + "com.atproto.server.createSession", + %{identifier: username, password: pw}, + _ do + {200, Hipdster.Auth.Session.new(username, pw)} + end + + XRPC.procedure c, "com.atproto.server.refreshSession", _, ctx do + case ctx do + %{user: %Hipdster.User{}, token_type: :refresh} -> + c.req_headers + |> Enum.into(%{}) + |> Map.get("authorization") + |> case do + "Bearer " <> token -> + case Hipdster.Auth.Session.refresh(token) do + %{} = session -> {200, session} + _ -> {400, %{error: "InvalidToken", message: "Refresh session failed"}} + end + + _ -> + {400, %{error: "InvalidToken", message: "Refresh session failed"}} + end + + _ -> + {401, %{error: "Unauthorized", message: "Not authorized"}} + end + end + + XRPC.procedure conn, "com.atproto.server.deleteSession", _, _ do + conn.req_headers + |> Enum.into(%{}) + |> Map.get("authorization") + |> case do + "Bearer " <> token -> + case Hipdster.Auth.Session.delete(token) do + :ok -> {200, %{}} + _ -> {401, %{error: "InvalidToken", message: "Delete session failed"}} + end + + _ -> + {401, %{error: "InvalidToken", message: "Delete session failed"}} + end + end + + XRPC.procedure _, "app.bsky.actor.putPreferences", %{preferences: prefs}, ctx do + case ctx do + %{user: %Hipdster.User{} = user, token_type: :access} -> + Hipdster.User.Preferences.put(user, prefs) + {200, {:blob, %{mime_type: "application/octet-stream", data: ""}}} + + _ -> + {401, %{error: "Unauthorized", message: "Not authorized"}} + end + end +end diff --git a/lib/hipdster/service/xrpc.ex b/lib/hipdster/service/xrpc.ex new file mode 100644 index 0000000..58e57fd --- /dev/null +++ b/lib/hipdster/service/xrpc.ex @@ -0,0 +1,22 @@ +defmodule Hipdster.XRPC do + @moduledoc """ + Just some macros to make the XRPC interface easier to write + """ + + defmacro query(conn, method, params, ctx, [do: block]) do + quote do + def xrpc_query(unquote(conn), unquote(method), unquote(params), unquote(ctx)) do + unquote(block) + end + end + end + + defmacro procedure(conn, method, params, ctx, [do: block]) do + quote do + def xrpc_procedure(unquote(conn), unquote(method), unquote(params), unquote(ctx)) do + unquote(block) + end + end + end + +end diff --git a/lib/hipdster/service/xrpc/listBlobs.ex b/lib/hipdster/service/xrpc/listBlobs.ex new file mode 100644 index 0000000..7e12f33 --- /dev/null +++ b/lib/hipdster/service/xrpc/listBlobs.ex @@ -0,0 +1,38 @@ +defmodule Hipdster.Xrpc.Query.ListBlobs do + import Ecto.Query + + @spec ecto_query_for(String.t(), String.t()) :: Ecto.Query.t() + def ecto_query_for(did, tid) do + after_timestamp = + Hipdster.Tid.from_string(tid).timestamp + |> DateTime.from_unix!(:microsecond) + + from b in Hipdster.Blob, + where: b.did == ^did, + where: b.inserted_at > ^after_timestamp, + order_by: [desc: b.inserted_at, desc: b.id], + select: b.cid + end + + def list_blobs(did, tid \\ nil, limit \\ nil, cursor \\ nil) do + query = ecto_query_for(did, tid || "#{Hipdster.Tid.empty()}") + + cids = + if cursor do + last = + Hipdster.Blob + |> where(cid: ^cursor) + |> Hipdster.Database.one() + + from b in query, where: b.inserted_at < ^last.inserted_at, limit: ^limit + else + from b in query, limit: ^limit + end + |> Hipdster.Database.all() + + %{ + cids: cids, + cursor: to_string(List.last(cids)) + } + end +end diff --git a/lib/hipdster/tid.ex b/lib/hipdster/tid.ex index b36411a..0313ca7 100644 --- a/lib/hipdster/tid.ex +++ b/lib/hipdster/tid.ex @@ -1,7 +1,20 @@ defmodule Hipdster.Tid do import Bitwise + import TypeClass + defstruct [:timestamp, :clock_id] + @b32_charset "234567abcdefghijklmnopqrstuvwxyz" + + @typedoc """ + A number of microseconds since the UNIX epoch, as a 64-bit non-negative integer + """ + @type unix_microseconds :: non_neg_integer() + + @typedoc """ + A random number in the range 0..1023 + """ + @type clock_id :: 0..1023 @typedoc """ A TID is a 13-character string. @@ -23,14 +36,8 @@ defmodule Hipdster.Tid do This struct holds the timestamp in microseconds since the UNIX epoch, and the clock_id, which is a random number in the range 0..1023. """ - @type t :: %__MODULE__{timestamp: unix_microseconds(), clock_id: non_neg_integer()} - - @typedoc """ - A number of microseconds since the UNIX epoch, as a 64-bit non-negative integer - """ - @type unix_microseconds :: non_neg_integer() + @type t :: %__MODULE__{timestamp: unix_microseconds(), clock_id: clock_id()} - @b32_charset "234567abcdefghijklmnopqrstuvwxyz" @spec from_string(String.t()) :: t() | {:error, String.t()} def from_string(str) when is_binary(str) and byte_size(str) == 13 do @@ -66,7 +73,7 @@ defmodule Hipdster.Tid do end end - defimpl String.Chars, for: Hipdster.Tid do + defimpl String.Chars do @spec to_string(Hipdster.Tid.t()) :: String.t() defdelegate to_string(tid), to: Hipdster.Tid end @@ -114,6 +121,32 @@ defmodule Hipdster.Tid do @spec now() :: t() def now do - %__MODULE__{timestamp: :os.system_time(:microsecond), clock_id: :rand.uniform(1 <<< 10)} + %__MODULE__{timestamp: System.os_time(:microsecond), clock_id: :rand.uniform(1 <<< 10)} + end + + @spec empty() :: t() + def empty do + %__MODULE__{timestamp: 0, clock_id: 0} + end + + defimpl TypeClass.Property.Generator do + def generate(_), do: %Hipdster.Tid{timestamp: :rand.uniform(1000000000), clock_id: :rand.uniform(1 <<< 10)} + end + + definst Witchcraft.Setoid do + @spec equivalent?( + Hipdster.Tid.t(), + Hipdster.Tid.t() + ) :: boolean() + def equivalent?(tid1, tid2) do + [tid1.timestamp, tid1.clock_id] == [tid2.timestamp, tid2.clock_id] + end + end + + definst Witchcraft.Ord do + import Bitwise + def compare(tid1, tid2) do + Witchcraft.Ord.compare(tid1.timestamp <<< 10 ||| tid1.clock_id, tid2.timestamp <<< 10 ||| tid2.clock_id) + end end end diff --git a/lib/main.ex b/lib/main.ex index 0362edd..4efa1e6 100644 --- a/lib/main.ex +++ b/lib/main.ex @@ -3,6 +3,16 @@ defmodule Hipdster.Application do @impl Application def start(_type, _args) do - Hipdster.Multicodec.start_link() + Hipdster.Database.Mnesia.create_tables() + Supervisor.start_link( + [ + {Bandit, plug: Hipdster.Http, scheme: :http, port: Application.get_env(:hipdster, :port)}, + {Hipdster.Multicodec, Application.get_env(:hipdster, :multicodec_csv_path)}, + {Hipdster.Database, []}, + {Hipdster.Auth.Session.Cleaner, []} + ], + strategy: :one_for_one, + name: Hipdster.Supervisor + ) end end diff --git a/lib/mix/tasks/mnesia.ex b/lib/mix/tasks/mnesia.ex new file mode 100644 index 0000000..de29714 --- /dev/null +++ b/lib/mix/tasks/mnesia.ex @@ -0,0 +1,21 @@ +defmodule Mix.Tasks.Mnesia.Setup do + use Mix.Task + + @nodes [node()] + + @impl Mix.Task + def run(_) do + # Create the DB directory (if custom path given) + if path = Application.get_env(:mnesia, :dir) do + :ok = File.mkdir_p!(path) + end + + # Create the Schema + Memento.stop() + Memento.Schema.create(@nodes) + Memento.start() + + + Enum.each(Hipdster.Database.Mnesia.tables(), &Memento.Table.create(&1, disc_copies: @nodes)) + end +end diff --git a/mix.exs b/mix.exs index 057db59..7af8dbc 100644 --- a/mix.exs +++ b/mix.exs @@ -29,9 +29,21 @@ defmodule Hipdster.MixProject do {:ex_doc, "~> 0.28.0", only: :dev, runtime: false}, {:multibase, "~> 0.0.1"}, {:ex_multihash, "~> 2.0.0"}, - {:rustler, "~> 0.31.0"}, + {:rustler, "~> 0.32"}, {:toml, "~> 0.7.0"}, {:varint, "~> 1.4"}, + {:plug, "~> 1.15.3"}, + {:cors_plug, "~> 3.0"}, + {:bandit, "~> 1.3.0"}, + {:argon2_elixir, "~> 4.0"}, + {:memento, "~> 0.3.2"}, + {:infer, "~> 0.2.6"}, + {:ecto, "~> 3.11.2"}, + {:ecto_sqlite3, "~> 0.15"}, + {:matcha, "~> 0.1.10"}, + {:witchcraft, "~> 1.0.4"}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, # type checking + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, # for linting ] end end diff --git a/mix.lock b/mix.lock index 4dc4c74..af69438 100644 --- a/mix.lock +++ b/mix.lock @@ -1,42 +1,60 @@ %{ + "argon2_elixir": {:hex, :argon2_elixir, "4.0.0", "7f6cd2e4a93a37f61d58a367d82f830ad9527082ff3c820b8197a8a736648941", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f9da27cf060c9ea61b1bd47837a28d7e48a8f6fa13a745e252556c14f9132c7f"}, + "bandit": {:hex, :bandit, "1.3.0", "6a4e8d7c9ea721edd02c389e2cc867890cd96f83116e71ddf1ccbdd80661550c", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "bda37d6c614d74778a5dc43b8bcdc3245cd30619eab0342f58042f968f2165da"}, "base1": {:hex, :base1, "0.1.0", "493a46bb5b34bf38d3b69ea6eaff33416d222f1621231cb1a04af42a351117ea", [:mix], [], "hexpm", "57cb13fe05f3dc4174c7fbca147b94dcfdceea1ce87fdd5f803a29d3ca75fc96"}, "base2": {:hex, :base2, "0.1.0", "bc9e19f1578e1439010d26b1bee456f3ac980481a46c3643b24e15c40e1db0ed", [:mix], [], "hexpm", "a25eb2892e03f609fd5634aaf9a7795fb90f98dc9a7a495ed8c03b2844bbb647"}, "basefiftyeight": {:hex, :basefiftyeight, "0.1.0", "3d48544743bf9aab7ab02aed803ac42af77acf268c7d8c71d4f39e7fa85ee8d3", [:mix], [], "hexpm", "af12f551429528c711e98628c029ad48d1e5ba5a284f40b2d91029a65381837a"}, - "bitcoinex": {:hex, :bitcoinex, "0.1.7", "2357049df10ba79e701138481692326863553476fd902c1c5a6e918d7f93b721", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "06f2d2c84b98ada2cf87106e54b7cc309497281f2bbf601e86b8528cfbd6e9fd"}, - "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, - "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, - "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, - "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, + "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, + "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"}, + "elixir_make": {:hex, :elixir_make, "0.8.3", "d38d7ee1578d722d89b4d452a3e36bcfdc644c618f0d063b874661876e708683", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "5c99a18571a756d4af7a4d89ca75c28ac899e6103af6f223982f09ce44942cc9"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.28.6", "2bbd7a143d3014fc26de9056793e97600ae8978af2ced82c2575f130b7c0d7d7", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bca1441614654710ba37a0e173079273d619f9160cbcc8cd04e6bd59f1ad0e29"}, "ex_multihash": {:hex, :ex_multihash, "2.0.0", "7fb36f842a2ec1c6bbba550f28fcd16d3c62981781b9466c9c1975c43d7db43c", [:mix], [], "hexpm", "66a08a86a1ba00d95736c595d7975696e5691308cdf7770c50b0f84a2a1172b0"}, - "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.2", "33398c172813b90fab9ab75c12b98d16cfab472c6dcbde832b13c45ce1c01947", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f3b1bf56e6992e28b9d86e3bf741a4aca3e641052eb47d13ae4f5f4d4944bdaf"}, - "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, - "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, + "exceptional": {:hex, :exceptional, "2.1.3", "cb17cb9b7c4882e763b82db08ba317678157ca95970fae96b31b3c90f5960c3d", [:mix], [], "hexpm", "59d67ae2df6784e7a957087742ae9011f220c3d1523706c5cd7ee0741bca5897"}, + "exqlite": {:hex, :exqlite, "0.21.0", "8d06c60b3d6df42bb4cdeb4dce4bc804788e227cead7dc190c3ffaba50bffbb4", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "b177180bb2788b761ddd5949763640aef92ed06db80d70a1130b6bede180b45f"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "infer": {:hex, :infer, "0.2.6", "0b8a9a08cf67c5ed1c1a29cc6bff81781cc30565bcd33959e60bd7a41056fccf", [:mix], [], "hexpm", "0e5ad1b7ba3fb2bbf32b36223add30432c11ab33a34b038ffa16a591813e2667"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "matcha": {:hex, :matcha, "0.1.10", "1a504638dae246bc4c60970632f448a7768d61d8e20833c717dce50a00d3d5c6", [:mix], [{:recon, ">= 2.3.0", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "ea83b1753de38294191301483fbe34b97e812605205b89c581b97336c9b3e7eb"}, + "memento": {:hex, :memento, "0.3.2", "38cfc8ff9bcb1adff7cbd0f3b78a762636b86dff764729d1c82d0464c539bdd0", [:mix], [], "hexpm", "25cf691a98a0cb70262f4a7543c04bab24648cb2041d937eb64154a8d6f8012b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "monad": {:hex, :monad, "1.0.5", "bd02263a8dad0894433ca3283ebb6f71a55799e1cd17bda1e8b2ea9e14eeb9c5", [:mix], [], "hexpm", "d8ebe20971160e96bd6cdf11b5e8b5c24b70fddde3d198e5f7c3b5ebfbc78d6e"}, "multibase": {:hex, :multibase, "0.0.1", "9243bebe6f7f0f9f873c770ddcddab285cc554a8dd2374b8de6b2add087eccbc", [:mix], [{:base1, "~> 0.1.0", [hex: :base1, repo: "hexpm", optional: false]}, {:base2, "~> 0.1.0", [hex: :base2, repo: "hexpm", optional: false]}, {:basefiftyeight, "~> 0.1.0", [hex: :basefiftyeight, repo: "hexpm", optional: false]}, {:zbase32, "~> 2.0.0", [hex: :zbase32, repo: "hexpm", optional: false]}], "hexpm", "6ea13a19e47da727c2def81b241ad10a39440c5705a4abed18e8310b46f45f51"}, - "multicodec": {:git, "https://github.com/ShreyanJain9/ex-multicodec.git", "7570cd559691e414a67223e9e8a7fd4ad2d7549b", [branch: "master"]}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, + "operator": {:hex, :operator, "0.2.1", "4572312bbd3e63a5c237bf15c3a7670d568e3651ea744289130780006e70e5f5", [:mix], [], "hexpm", "1990cc6dc651d7fff04636eef06fc64e6bc1da83a1da890c08ca3432e17e267a"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "rustler": {:hex, :rustler, "0.31.0", "7e5eefe61e6e6f8901e5aa3de60073d360c6320d9ec363027b0197297b80c46a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "99e378459bfb9c3bda6d3548b2b3bc6f9ad97f728f76bdbae7bf5c770a4f8abd"}, - "rustler_precompiled": {:hex, :rustler_precompiled, "0.7.1", "ecadf02cc59a0eccbaed6c1937303a5827fbcf60010c541595e6d3747d3d0f9f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "b9e4657b99a1483ea31502e1d58c464bedebe9028808eda45c3a429af4550c66"}, + "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, + "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "quark": {:hex, :quark, "2.3.2", "066e0d431440d077684469967f54d732443ea2a48932e0916e974633e8b39c95", [:mix], [], "hexpm", "2f6423779b02afe7e3e4af3cfecfcd94572f2051664d4d8329ffa872d24b10a8"}, + "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"}, + "rustler": {:hex, :rustler, "0.32.1", "f4cf5a39f9e85d182c0a3f75fa15b5d0add6542ab0bf9ceac6b4023109ebd3fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "b96be75526784f86f6587f051bc8d6f4eaff23d6e0f88dbcfe4d5871f52946f7"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, - "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, + "type_class": {:hex, :type_class, "1.2.8", "349db84be8c664e119efaae1a09a44b113bc8e81af1d032f4e3e38feef4fac32", [:mix], [{:exceptional, "~> 2.1", [hex: :exceptional, repo: "hexpm", optional: false]}], "hexpm", "bb93de2cacfd6f0ee43f4616f7a139816a73deba4ae8ee3364bcfa4abe3eef3e"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "varint": {:hex, :varint, "1.4.0", "b7405c8a99db7b95d4341fa9cb15e7c3af6c8dda43e21bbe1c4a9cdff50b6502", [:mix], [], "hexpm", "0fd461901b7120c03467530dff3c58fa3475328fd75ba72c7d3cbf13bce6b0d2"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "witchcraft": {:hex, :witchcraft, "1.0.4", "8733ac0ee769d4d2f73610de5a2b601a4ccbe385d1fca6419280f2511d21d0c9", [:mix], [{:exceptional, "~> 2.1", [hex: :exceptional, repo: "hexpm", optional: false]}, {:operator, "~> 0.2", [hex: :operator, repo: "hexpm", optional: false]}, {:quark, "~> 2.2", [hex: :quark, repo: "hexpm", optional: false]}, {:type_class, "~> 1.2", [hex: :type_class, repo: "hexpm", optional: false]}], "hexpm", "a380f439f1962d2e56cdad874ed7eb4612ddd6ec5ee3c6ad0c5d63e60539e6b0"}, "zbase32": {:hex, :zbase32, "2.0.0", "5a61d5ee8f39092d4a243da2a42b5b4339ef226d9b182603f63d5a3f16d192ee", [:mix], [], "hexpm", "798f81895658f9773e1dcf30ba3c118547f482502c5e1e19e72752f9a6f23e44"}, } diff --git a/native/hipdster_auth_jwt_internal/.cargo/config.toml b/native/hipdster_auth_jwt_internal/.cargo/config.toml new file mode 100644 index 0000000..20f03f3 --- /dev/null +++ b/native/hipdster_auth_jwt_internal/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.'cfg(target_os = "macos")'] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] diff --git a/native/hipdster_auth_jwt_internal/.gitignore b/native/hipdster_auth_jwt_internal/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/native/hipdster_auth_jwt_internal/.gitignore @@ -0,0 +1 @@ +/target diff --git a/native/hipdster_auth_jwt_internal/Cargo.lock b/native/hipdster_auth_jwt_internal/Cargo.lock new file mode 100644 index 0000000..e6a7821 --- /dev/null +++ b/native/hipdster_auth_jwt_internal/Cargo.lock @@ -0,0 +1,1190 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "binstring" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0d60973d9320722cb1206f412740e162a33b8547ea8d6be75d7cff237c7a85" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boring" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8259fc1ea91894a550190683fbcda307d1ef85f2875d93a2b1ab3cab58b62fea" +dependencies = [ + "bitflags", + "boring-sys", + "foreign-types", + "libc", + "once_cell", +] + +[[package]] +name = "boring-sys" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace69f2e0d89d2c5e0874efe47f46259ff794fe8cbddfca72132c817611ad471" +dependencies = [ + "bindgen", + "cmake", + "fs_extra", + "fslock", +] + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "coarsetime" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b3839cf01bb7960114be3ccf2340f541b6d0c81f8690b007b2b39f750f7e5d" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ct-codecs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hipdster_auth_jwt_internal" +version = "0.1.0" +dependencies = [ + "jwt-simple", + "rustler", + "serde_json", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha1-compact" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9d405ec732fa3fcde87264e54a32a84956a377b3e3107de96e59b798c84a7" + +[[package]] +name = "hmac-sha256" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ce1f4656bae589a3fab938f9f09bf58645b7ed01a2c5f8a3c238e01a4ef78a" +dependencies = [ + "digest", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jwt-simple" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094661f5aad510abe2658bff20409e89046b753d9dc2d4007f5c100b6d982ba0" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "boring", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand", + "serde", + "serde_json", + "superboring", + "thiserror", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustler" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c330a01eaed3ebce4708e2f1052e0676a9155c1583b8afadc69acaf6105e33" +dependencies = [ + "lazy_static", + "rustler_codegen", + "rustler_sys", +] + +[[package]] +name = "rustler_codegen" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28516544e4ab5fd4c6802343d9676540fbbac1489d36c0898ad8c19ac11f5be2" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustler_sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e21c0f1bc2458e29df0249e0b6a047af44303c73856c179098b6fc3700fd38" +dependencies = [ + "regex", + "unreachable", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "superboring" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbde97f499e51ef384f585dc8f8fb6a9c3a71b274b8d12469b516758e6540607" +dependencies = [ + "getrandom", + "hmac-sha256", + "hmac-sha512", + "rand", + "rsa", +] + +[[package]] +name = "syn" +version = "2.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/native/hipdster_auth_jwt_internal/Cargo.toml b/native/hipdster_auth_jwt_internal/Cargo.toml new file mode 100644 index 0000000..29c007f --- /dev/null +++ b/native/hipdster_auth_jwt_internal/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "hipdster_auth_jwt_internal" +version = "0.1.0" +authors = [] +edition = "2021" + +[lib] +name = "hipdster_auth_jwt_internal" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +jwt-simple = "0.12.9" +serde_json = "1.0" +rustler = "0.32.1" diff --git a/native/hipdster_auth_jwt_internal/README.md b/native/hipdster_auth_jwt_internal/README.md new file mode 100644 index 0000000..3bbee84 --- /dev/null +++ b/native/hipdster_auth_jwt_internal/README.md @@ -0,0 +1,20 @@ +# NIF for Elixir.Hipdster.Auth.JWT.Internal + +## To build the NIF module: + +- Your NIF will now build along with your project. + +## To load the NIF: + +```elixir +defmodule Hipdster.Auth.JWT.Internal do + use Rustler, otp_app: :hipdster, crate: "hipdster_auth_jwt_internal" + + # When your NIF is loaded, it will override this function. + def generate_jwt(account_did, service_did, key, is_k256,), do: :erlang.nif_error(:nif_not_loaded) +end +``` + +## Examples + +[This](https://github.com/rusterlium/NifIo) is a complete example of a NIF written in Rust. diff --git a/native/hipdster_auth_jwt_internal/src/lib.rs b/native/hipdster_auth_jwt_internal/src/lib.rs new file mode 100644 index 0000000..0096d9e --- /dev/null +++ b/native/hipdster_auth_jwt_internal/src/lib.rs @@ -0,0 +1,91 @@ +use jwt_simple::prelude::*; +use rustler::{Binary, Encoder, Env, Term}; +use std::str; + +mod atoms { + rustler::atoms! { + ok, + error, + } +} + +#[rustler::nif] +fn generate_k256_jwt<'a>( + env: Env<'a>, + account_did: Binary<'a>, + service_did: Binary<'a>, + subject: Binary<'a>, + private_key: Binary<'a>, +) -> Term<'a> { + let signing_key = match ES256kKeyPair::from_bytes(&private_key) { + Ok(key) => key, + Err(e) => { + return ( + atoms::error(), + format!("Failed to create secret key: {}", e), + ) + .encode(env) + } + }; + + let service_did_string = str::from_utf8(service_did.as_slice()).unwrap(); + let account_did = str::from_utf8(account_did.as_slice()).unwrap(); + + let subject = str::from_utf8(subject.as_slice()).unwrap(); + + let claim = Claims::create(Duration::from_mins(1)) + .with_issuer(account_did) + .with_audience(service_did_string); + + let claim = match subject { + "" => claim, + s => claim.with_subject(s), + }; + let token = match signing_key.sign(claim) { + Ok(token) => token, + Err(e) => return (atoms::error(), format!("Failed to sign token: {}", e)).encode(env), + }; + + let token = token.as_str(); + (atoms::ok(), token).encode(env) +} + + +#[rustler::nif] +fn generate_hs256_jwt<'a>( + env: Env<'a>, + account_did: Binary<'a>, + subject: Binary<'a>, + hs256_secret: Binary<'a>, + time_in_minutes: i64, +) -> Term<'a> { + let signing_key = HS256Key::from_bytes(&hs256_secret); + + let account_did = str::from_utf8(account_did.as_slice()).unwrap(); + let subject = str::from_utf8(subject.as_slice()).unwrap(); + + let claim = Claims::create(Duration::from_mins(time_in_minutes as u64)) + .with_issuer(account_did) + .with_subject(subject); + + let token = match signing_key.authenticate(claim) { + Ok(token) => token, + Err(e) => return (atoms::error(), format!("Failed to sign token: {}", e)).encode(env), + }; + + let token = token.as_str(); + (atoms::ok(), token).encode(env) +} + +#[rustler::nif] +fn verify_hs256_jwt<'a>(env: Env<'a>, jwt: Binary<'a>, hs256_secret: Binary<'a>) -> Term<'a> { + let signing_key = HS256Key::from_bytes(&hs256_secret); + + match signing_key.verify_token::(str::from_utf8(jwt.as_slice()).unwrap(), None) { + Ok(token) => (atoms::ok(), token.serialize(serde_json::value::Serializer).unwrap().to_string()), + Err(e) => (atoms::error(), format!("Failed to verify token: {}", e)), + }.encode(env) +} + + +rustler::init!("Elixir.Hipdster.Auth.JWT.Internal", [generate_k256_jwt, generate_hs256_jwt, verify_hs256_jwt]); diff --git a/native/hipdster_dagcbor_internal/Cargo.toml b/native/hipdster_dagcbor_internal/Cargo.toml index f5a12c7..d7de0aa 100644 --- a/native/hipdster_dagcbor_internal/Cargo.toml +++ b/native/hipdster_dagcbor_internal/Cargo.toml @@ -11,8 +11,8 @@ crate-type = ["cdylib"] [dependencies] libipld = "0.16.0" -rustler = "0.31.0" -serde_ipld_dagcbor = "0.4.2" +rustler = "0.32.0" +data-encoding = "2.5.0" serde_json = "1.0.113" [features] "preserve_order" = [] diff --git a/native/hipdster_dagcbor_internal/src/lib.rs b/native/hipdster_dagcbor_internal/src/lib.rs index b4dc699..38bf15e 100644 --- a/native/hipdster_dagcbor_internal/src/lib.rs +++ b/native/hipdster_dagcbor_internal/src/lib.rs @@ -3,14 +3,16 @@ use rustler::Env; use rustler::NifResult; use rustler::Term; use rustler::Binary; -use serde_json::from_str; -use serde_json::Value; +use serde_json::Value as JsonValue; +use serde_json::json; use libipld::Ipld; use libipld::Cid; use libipld::codec::Codec; use libipld::cbor::DagCborCodec; use std::collections::BTreeMap; use std::str::FromStr; +use data_encoding::BASE64_NOPAD; + mod atoms { rustler::atoms! { @@ -19,12 +21,13 @@ mod atoms { } } -pub fn json_to_ipld(val: Value) -> Ipld { +// taken from bnewbold/adenosine (https://gitlab.com/bnewbold/adenosine) +pub fn json_to_ipld(val: JsonValue) -> Ipld { match val { - Value::Null => Ipld::Null, - Value::Bool(b) => Ipld::Bool(b), - Value::String(s) => Ipld::String(s), - Value::Number(v) => { + JsonValue::Null => Ipld::Null, + JsonValue::Bool(b) => Ipld::Bool(b), + JsonValue::String(s) => Ipld::String(s), + JsonValue::Number(v) => { if let Some(f) = v.as_f64() { if v.is_i64() { Ipld::Integer(v.as_i64().unwrap().into()) @@ -37,8 +40,8 @@ pub fn json_to_ipld(val: Value) -> Ipld { Ipld::Null } }, - Value::Array(l) => Ipld::List(l.into_iter().map(json_to_ipld).collect()), - Value::Object(m) => { + JsonValue::Array(l) => Ipld::List(l.into_iter().map(json_to_ipld).collect()), + JsonValue::Object(m) => { let map: BTreeMap = BTreeMap::from_iter(m.into_iter().map(|(k, v)| { if k == "cid" && v.is_string() { (k, Ipld::Link(Cid::from_str(v.as_str().unwrap()).unwrap())) @@ -51,9 +54,27 @@ pub fn json_to_ipld(val: Value) -> Ipld { } } +// Taken from bnewbold/adenosine +pub fn ipld_to_json(val: Ipld) -> JsonValue { + match val { + Ipld::Null => JsonValue::Null, + Ipld::Bool(b) => JsonValue::Bool(b), + Ipld::Integer(v) => json!(v), + Ipld::Float(v) => json!(v), + Ipld::String(s) => JsonValue::String(s), + Ipld::Bytes(b) => JsonValue::String(BASE64_NOPAD.encode(&b)), + Ipld::List(l) => JsonValue::Array(l.into_iter().map(ipld_to_json).collect()), + Ipld::Map(m) => JsonValue::Object(serde_json::Map::from_iter( + m.into_iter().map(|(k, v)| (k, ipld_to_json(v))), + )), + Ipld::Link(c) => JsonValue::String(c.to_string()), + } +} + + #[rustler::nif] fn encode_dag_cbor(env: Env, json: String) -> NifResult { - let parsed_json: serde_json::Value = match from_str(&json) { + let parsed_json: JsonValue = match serde_json::from_str(&json) { Ok(json) => json, Err(e) => return Ok((atoms::error(), format!("Failed to parse JSON: {}", e)).encode(env)), }; @@ -81,17 +102,23 @@ fn encode_dag_cbor(env: Env, json: String) -> NifResult { #[rustler::nif] fn decode_dag_cbor<'a>(env: Env<'a>, cbor_data: Binary<'a>) -> Term<'a> { - let parsed_cbor: serde_json::Value = match serde_ipld_dagcbor::from_slice(&cbor_data) { + let decoded_cbor = match DagCborCodec.decode(&cbor_data) { Ok(cbor) => cbor, - Err(e) => return (atoms::error(), format!("Failed to parse DAG-CBOR: {}", e)).encode(env), + Err(e) => { + return ( + atoms::error(), + format!("Failed to decode from DAG-CBOR: {}", e), + ) + .encode(env) + } }; - let json = match serde_json::to_string(&parsed_cbor) { - Ok(json) => json, - Err(e) => return (atoms::error(), format!("Failed to encode to JSON: {}", e)).encode(env), - }; + let json = ipld_to_json(decoded_cbor); - (atoms::ok(), json).encode(env) + match serde_json::to_string(&json) { + Ok(string) => (atoms::ok(), string).encode(env), + Err(e) => return (atoms::error(), format!("Failed to serialize JSON: {}", e)).encode(env), + } } rustler::init!("Elixir.Hipdster.DagCBOR.Internal", [encode_dag_cbor, decode_dag_cbor]); diff --git a/native/hipdster_k256_internal/.gitignore b/native/hipdster_k256_internal/.gitignore index 6985cf1..ae64884 100644 --- a/native/hipdster_k256_internal/.gitignore +++ b/native/hipdster_k256_internal/.gitignore @@ -12,3 +12,4 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +.mnesia/ diff --git a/native/hipdster_k256_internal/Cargo.toml b/native/hipdster_k256_internal/Cargo.toml index 4746b48..1447766 100644 --- a/native/hipdster_k256_internal/Cargo.toml +++ b/native/hipdster_k256_internal/Cargo.toml @@ -14,7 +14,7 @@ atoms = "2.2.3" elliptic-curve = "0.13.8" hex = "0.4.3" k256 = "0.13.3" -rustler = "0.31.0" +rustler = "0.32.0" [features] "ecdsa" = ["k256/ecdsa"] "arithmetic" = ["k256/arithmetic"] diff --git a/priv/database/migrations/20240407035653_create_users.exs b/priv/database/migrations/20240407035653_create_users.exs new file mode 100644 index 0000000..c62e700 --- /dev/null +++ b/priv/database/migrations/20240407035653_create_users.exs @@ -0,0 +1,17 @@ +defmodule Hipdster.Database.Migrations.CreateUsers do + use Ecto.Migration + + def change do + create table(:users) do + add :did, :string + add :handle, :string + add :password_hash, :string + add :signing_key, :binary + add :rotation_key, :binary + add :data, :map + end + + create unique_index(:users, [:did]) + create unique_index(:users, [:handle]) + end +end diff --git a/priv/database/migrations/20240408194325_create_blobs.exs b/priv/database/migrations/20240408194325_create_blobs.exs new file mode 100644 index 0000000..f542b28 --- /dev/null +++ b/priv/database/migrations/20240408194325_create_blobs.exs @@ -0,0 +1,16 @@ +defmodule Hipdster.Database.Migrations.CreateBlobs do + use Ecto.Migration + + def change do + create table(:blobs) do + add :hash, :binary + add :cid, :string + add :mime_type, :string + add :data, :binary + add :did, :string + end + create unique_index(:blobs, [:hash]) + create index(:blobs, [:did]) + create index(:blobs, [:cid]) + end +end diff --git a/priv/database/migrations/20240408212617_add_blob_timestamps.exs b/priv/database/migrations/20240408212617_add_blob_timestamps.exs new file mode 100644 index 0000000..b233081 --- /dev/null +++ b/priv/database/migrations/20240408212617_add_blob_timestamps.exs @@ -0,0 +1,9 @@ +defmodule Hipdster.Database.Migrations.AddBlobTimestamps do + use Ecto.Migration + + def change do + alter table(:blobs) do + timestamps() + end + end +end diff --git a/priv/database/migrations/20240409034124_add_pagination_indexes.exs b/priv/database/migrations/20240409034124_add_pagination_indexes.exs new file mode 100644 index 0000000..3c0385d --- /dev/null +++ b/priv/database/migrations/20240409034124_add_pagination_indexes.exs @@ -0,0 +1,7 @@ +defmodule Hipdster.Database.Migrations.AddPaginationIndexes do + use Ecto.Migration + + def change do + create index(:blobs, [:inserted_at, :id]) + end +end