;; identity/clients.sx — the OAuth client registry (RFC 6749 §2).
;;
;; A client is registered with a type, a secret, and its allow-listed
;; redirect_uris:
;;
;;   public        — cannot keep a secret (SPAs, native apps, §2.1);
;;                   identified but not authenticated.
;;   confidential  — can authenticate; MUST present its secret at the token
;;                   endpoint (§3.2.1, §4.1.3). A wrong secret is
;;                   invalid_client — never a soft pass.
;;
;; Redirect URIs must be pre-registered (§3.1.2.2 + OAuth Security BCP):
;; valid_redirect/3 is the exact-match check the authorize/exchange steps
;; consult so an attacker cannot redirect the code to an unregistered URI.
;;
;;   register(C, ClientId, Type, Secret, RedirectUris) -> ok | {error, exists}
;;   lookup(C, ClientId)            -> {ok, Type, RedirectUris} | {error, unknown_client}
;;   authenticate(C, ClientId, Sec) -> {ok, public} | {ok, confidential}
;;                                   | {error, invalid_client} | {error, unknown_client}
;;   valid_redirect(C, ClientId, U) -> true | false

(define
  identity-clients-source
  "-module(identity_clients).\n\n   start() ->\n     spawn(fun () -> loop([]) end).\n\n   register(C, ClientId, Type, Secret, RedirectUris) ->\n     C ! {register, ClientId, Type, Secret, RedirectUris, self()},\n     receive {client_reply, R} -> R end.\n\n   lookup(C, ClientId) ->\n     C ! {lookup, ClientId, self()},\n     receive {client_reply, R} -> R end.\n\n   authenticate(C, ClientId, Secret) ->\n     C ! {authenticate, ClientId, Secret, self()},\n     receive {client_reply, R} -> R end.\n\n   valid_redirect(C, ClientId, Uri) ->\n     C ! {valid_redirect, ClientId, Uri, self()},\n     receive {client_reply, R} -> R end.\n\n   loop(Clients) ->\n     receive\n       {register, ClientId, Type, Secret, RedirectUris, From} ->\n         case find(ClientId, Clients) of\n           {ok, _} ->\n             From ! {client_reply, {error, exists}},\n             loop(Clients);\n           none ->\n             From ! {client_reply, ok},\n             loop([{ClientId, {Type, Secret, RedirectUris}} | Clients])\n         end;\n       {lookup, ClientId, From} ->\n         case find(ClientId, Clients) of\n           none -> From ! {client_reply, {error, unknown_client}};\n           {ok, {Type, _, Uris}} -> From ! {client_reply, {ok, Type, Uris}}\n         end,\n         loop(Clients);\n       {authenticate, ClientId, Secret, From} ->\n         case find(ClientId, Clients) of\n           none ->\n             From ! {client_reply, {error, unknown_client}};\n           {ok, {public, _, _}} ->\n             From ! {client_reply, {ok, public}};\n           {ok, {confidential, S, _}} ->\n             case S =:= Secret of\n               true -> From ! {client_reply, {ok, confidential}};\n               false -> From ! {client_reply, {error, invalid_client}}\n             end\n         end,\n         loop(Clients);\n       {valid_redirect, ClientId, Uri, From} ->\n         case find(ClientId, Clients) of\n           none -> From ! {client_reply, false};\n           {ok, {_, _, Uris}} -> From ! {client_reply, member(Uri, Uris)}\n         end,\n         loop(Clients);\n       {stop, From} ->\n         From ! {client_reply, ok}\n     end.\n\n   member(_, []) -> false;\n   member(X, [Y | Rest]) ->\n     case X =:= Y of\n       true -> true;\n       false -> member(X, Rest)\n     end.\n\n   find(_, []) -> none;\n   find(Key, [{K, V} | Rest]) ->\n     case K =:= Key of\n       true -> {ok, V};\n       false -> find(Key, Rest)\n     end.")

(define
  identity-load-clients!
  (fn () (erlang-load-module identity-clients-source)))
