detached 19d3b27

feat(session): add passkey list and remove RPCs

commit
19d3b270c8835632bcfe5edb0376aa1ecf07733b
author
brice · · 2026-10-12 14:47 UTC
parent
97a9365
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +420 -19
M gen/ermite/ermiteconnect/session.connect.go +70 -0
@@ -41,6 +41,12 @@ const (
4141 // SessionServiceSignOutEverywhereProcedure is the fully-qualified name of the SessionService's
4242 // SignOutEverywhere RPC.
4343 SessionServiceSignOutEverywhereProcedure = "/ermite.SessionService/SignOutEverywhere"
44+ // SessionServiceListPasskeysProcedure is the fully-qualified name of the SessionService's
45+ // ListPasskeys RPC.
46+ SessionServiceListPasskeysProcedure = "/ermite.SessionService/ListPasskeys"
47+ // SessionServiceRemovePasskeyProcedure is the fully-qualified name of the SessionService's
48+ // RemovePasskey RPC.
49+ SessionServiceRemovePasskeyProcedure = "/ermite.SessionService/RemovePasskey"
4450 )
4551
4652 // SessionServiceClient is a client for the ermite.SessionService service.
@@ -56,6 +62,14 @@ type SessionServiceClient interface {
5662 // other devices. Pending login links are not affected. Tokens are not
5763 // affected either: revoke them with TokenService.
5864 SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error)
65+ // ListPasskeys lists the owner's registered passkeys, oldest first. Fails
66+ // with failed_precondition when the server has passkeys disabled.
67+ ListPasskeys(context.Context, *connect.Request[ermite.ListPasskeysRequest]) (*connect.Response[ermite.ListPasskeysResponse], error)
68+ // RemovePasskey removes one passkey. It also signs out every browser
69+ // session, on every device, like SignOutEverywhere. Fails with not_found
70+ // for an unknown id, invalid_argument for an id that is not base64url, and
71+ // failed_precondition when the server has passkeys disabled.
72+ RemovePasskey(context.Context, *connect.Request[ermite.RemovePasskeyRequest]) (*connect.Response[ermite.RemovePasskeyResponse], error)
5973 }
6074
6175 // NewSessionServiceClient constructs a client for the ermite.SessionService service. By default, it
@@ -81,6 +95,18 @@ func NewSessionServiceClient(httpClient connect.HTTPClient, baseURL string, opts
8195 connect.WithSchema(sessionServiceMethods.ByName("SignOutEverywhere")),
8296 connect.WithClientOptions(opts...),
8397 ),
98+ listPasskeys: connect.NewClient[ermite.ListPasskeysRequest, ermite.ListPasskeysResponse](
99+ httpClient,
100+ baseURL+SessionServiceListPasskeysProcedure,
101+ connect.WithSchema(sessionServiceMethods.ByName("ListPasskeys")),
102+ connect.WithClientOptions(opts...),
103+ ),
104+ removePasskey: connect.NewClient[ermite.RemovePasskeyRequest, ermite.RemovePasskeyResponse](
105+ httpClient,
106+ baseURL+SessionServiceRemovePasskeyProcedure,
107+ connect.WithSchema(sessionServiceMethods.ByName("RemovePasskey")),
108+ connect.WithClientOptions(opts...),
109+ ),
84110 }
85111 }
86112
@@ -88,6 +114,8 @@ func NewSessionServiceClient(httpClient connect.HTTPClient, baseURL string, opts
88114 type sessionServiceClient struct {
89115 createLoginLink *connect.Client[ermite.CreateLoginLinkRequest, ermite.CreateLoginLinkResponse]
90116 signOutEverywhere *connect.Client[ermite.SignOutEverywhereRequest, ermite.SignOutEverywhereResponse]
117+ listPasskeys *connect.Client[ermite.ListPasskeysRequest, ermite.ListPasskeysResponse]
118+ removePasskey *connect.Client[ermite.RemovePasskeyRequest, ermite.RemovePasskeyResponse]
91119 }
92120
93121 // CreateLoginLink calls ermite.SessionService.CreateLoginLink.
@@ -100,6 +128,16 @@ func (c *sessionServiceClient) SignOutEverywhere(ctx context.Context, req *conne
100128 return c.signOutEverywhere.CallUnary(ctx, req)
101129 }
102130
131+// ListPasskeys calls ermite.SessionService.ListPasskeys.
132+func (c *sessionServiceClient) ListPasskeys(ctx context.Context, req *connect.Request[ermite.ListPasskeysRequest]) (*connect.Response[ermite.ListPasskeysResponse], error) {
133+ return c.listPasskeys.CallUnary(ctx, req)
134+}
135+
136+// RemovePasskey calls ermite.SessionService.RemovePasskey.
137+func (c *sessionServiceClient) RemovePasskey(ctx context.Context, req *connect.Request[ermite.RemovePasskeyRequest]) (*connect.Response[ermite.RemovePasskeyResponse], error) {
138+ return c.removePasskey.CallUnary(ctx, req)
139+}
140+
103141 // SessionServiceHandler is an implementation of the ermite.SessionService service.
104142 type SessionServiceHandler interface {
105143 // CreateLoginLink creates a one-time link that signs the owner's browser
@@ -113,6 +151,14 @@ type SessionServiceHandler interface {
113151 // other devices. Pending login links are not affected. Tokens are not
114152 // affected either: revoke them with TokenService.
115153 SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error)
154+ // ListPasskeys lists the owner's registered passkeys, oldest first. Fails
155+ // with failed_precondition when the server has passkeys disabled.
156+ ListPasskeys(context.Context, *connect.Request[ermite.ListPasskeysRequest]) (*connect.Response[ermite.ListPasskeysResponse], error)
157+ // RemovePasskey removes one passkey. It also signs out every browser
158+ // session, on every device, like SignOutEverywhere. Fails with not_found
159+ // for an unknown id, invalid_argument for an id that is not base64url, and
160+ // failed_precondition when the server has passkeys disabled.
161+ RemovePasskey(context.Context, *connect.Request[ermite.RemovePasskeyRequest]) (*connect.Response[ermite.RemovePasskeyResponse], error)
116162 }
117163
118164 // NewSessionServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -134,12 +180,28 @@ func NewSessionServiceHandler(svc SessionServiceHandler, opts ...connect.Handler
134180 connect.WithSchema(sessionServiceMethods.ByName("SignOutEverywhere")),
135181 connect.WithHandlerOptions(opts...),
136182 )
183+ sessionServiceListPasskeysHandler := connect.NewUnaryHandler(
184+ SessionServiceListPasskeysProcedure,
185+ svc.ListPasskeys,
186+ connect.WithSchema(sessionServiceMethods.ByName("ListPasskeys")),
187+ connect.WithHandlerOptions(opts...),
188+ )
189+ sessionServiceRemovePasskeyHandler := connect.NewUnaryHandler(
190+ SessionServiceRemovePasskeyProcedure,
191+ svc.RemovePasskey,
192+ connect.WithSchema(sessionServiceMethods.ByName("RemovePasskey")),
193+ connect.WithHandlerOptions(opts...),
194+ )
137195 return "/ermite.SessionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
138196 switch r.URL.Path {
139197 case SessionServiceCreateLoginLinkProcedure:
140198 sessionServiceCreateLoginLinkHandler.ServeHTTP(w, r)
141199 case SessionServiceSignOutEverywhereProcedure:
142200 sessionServiceSignOutEverywhereHandler.ServeHTTP(w, r)
201+ case SessionServiceListPasskeysProcedure:
202+ sessionServiceListPasskeysHandler.ServeHTTP(w, r)
203+ case SessionServiceRemovePasskeyProcedure:
204+ sessionServiceRemovePasskeyHandler.ServeHTTP(w, r)
143205 default:
144206 http.NotFound(w, r)
145207 }
@@ -156,3 +218,11 @@ func (UnimplementedSessionServiceHandler) CreateLoginLink(context.Context, *conn
156218 func (UnimplementedSessionServiceHandler) SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error) {
157219 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.SessionService.SignOutEverywhere is not implemented"))
158220 }
221+
222+func (UnimplementedSessionServiceHandler) ListPasskeys(context.Context, *connect.Request[ermite.ListPasskeysRequest]) (*connect.Response[ermite.ListPasskeysResponse], error) {
223+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.SessionService.ListPasskeys is not implemented"))
224+}
225+
226+func (UnimplementedSessionServiceHandler) RemovePasskey(context.Context, *connect.Request[ermite.RemovePasskeyRequest]) (*connect.Response[ermite.RemovePasskeyResponse], error) {
227+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.SessionService.RemovePasskey is not implemented"))
228+}
@@ -41,6 +41,12 @@ const (
41 // SessionServiceSignOutEverywhereProcedure is the fully-qualified name of the SessionService's41 // SessionServiceSignOutEverywhereProcedure is the fully-qualified name of the SessionService's
42 // SignOutEverywhere RPC.42 // SignOutEverywhere RPC.
43 SessionServiceSignOutEverywhereProcedure = "/ermite.SessionService/SignOutEverywhere"43 SessionServiceSignOutEverywhereProcedure = "/ermite.SessionService/SignOutEverywhere"
44+ // SessionServiceListPasskeysProcedure is the fully-qualified name of the SessionService's
45+ // ListPasskeys RPC.
46+ SessionServiceListPasskeysProcedure = "/ermite.SessionService/ListPasskeys"
47+ // SessionServiceRemovePasskeyProcedure is the fully-qualified name of the SessionService's
48+ // RemovePasskey RPC.
49+ SessionServiceRemovePasskeyProcedure = "/ermite.SessionService/RemovePasskey"
44 )50 )
45 51
46 // SessionServiceClient is a client for the ermite.SessionService service.52 // SessionServiceClient is a client for the ermite.SessionService service.
@@ -56,6 +62,14 @@ type SessionServiceClient interface {
56 // other devices. Pending login links are not affected. Tokens are not62 // other devices. Pending login links are not affected. Tokens are not
57 // affected either: revoke them with TokenService.63 // affected either: revoke them with TokenService.
58 SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error)64 SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error)
65+ // ListPasskeys lists the owner's registered passkeys, oldest first. Fails
66+ // with failed_precondition when the server has passkeys disabled.
67+ ListPasskeys(context.Context, *connect.Request[ermite.ListPasskeysRequest]) (*connect.Response[ermite.ListPasskeysResponse], error)
68+ // RemovePasskey removes one passkey. It also signs out every browser
69+ // session, on every device, like SignOutEverywhere. Fails with not_found
70+ // for an unknown id, invalid_argument for an id that is not base64url, and
71+ // failed_precondition when the server has passkeys disabled.
72+ RemovePasskey(context.Context, *connect.Request[ermite.RemovePasskeyRequest]) (*connect.Response[ermite.RemovePasskeyResponse], error)
59 }73 }
60 74
61 // NewSessionServiceClient constructs a client for the ermite.SessionService service. By default, it75 // NewSessionServiceClient constructs a client for the ermite.SessionService service. By default, it
@@ -81,6 +95,18 @@ func NewSessionServiceClient(httpClient connect.HTTPClient, baseURL string, opts
81 connect.WithSchema(sessionServiceMethods.ByName("SignOutEverywhere")),95 connect.WithSchema(sessionServiceMethods.ByName("SignOutEverywhere")),
82 connect.WithClientOptions(opts...),96 connect.WithClientOptions(opts...),
83 ),97 ),
98+ listPasskeys: connect.NewClient[ermite.ListPasskeysRequest, ermite.ListPasskeysResponse](
99+ httpClient,
100+ baseURL+SessionServiceListPasskeysProcedure,
101+ connect.WithSchema(sessionServiceMethods.ByName("ListPasskeys")),
102+ connect.WithClientOptions(opts...),
103+ ),
104+ removePasskey: connect.NewClient[ermite.RemovePasskeyRequest, ermite.RemovePasskeyResponse](
105+ httpClient,
106+ baseURL+SessionServiceRemovePasskeyProcedure,
107+ connect.WithSchema(sessionServiceMethods.ByName("RemovePasskey")),
108+ connect.WithClientOptions(opts...),
109+ ),
84 }110 }
85 }111 }
86 112
@@ -88,6 +114,8 @@ func NewSessionServiceClient(httpClient connect.HTTPClient, baseURL string, opts
88 type sessionServiceClient struct {114 type sessionServiceClient struct {
89 createLoginLink *connect.Client[ermite.CreateLoginLinkRequest, ermite.CreateLoginLinkResponse]115 createLoginLink *connect.Client[ermite.CreateLoginLinkRequest, ermite.CreateLoginLinkResponse]
90 signOutEverywhere *connect.Client[ermite.SignOutEverywhereRequest, ermite.SignOutEverywhereResponse]116 signOutEverywhere *connect.Client[ermite.SignOutEverywhereRequest, ermite.SignOutEverywhereResponse]
117+ listPasskeys *connect.Client[ermite.ListPasskeysRequest, ermite.ListPasskeysResponse]
118+ removePasskey *connect.Client[ermite.RemovePasskeyRequest, ermite.RemovePasskeyResponse]
91 }119 }
92 120
93 // CreateLoginLink calls ermite.SessionService.CreateLoginLink.121 // CreateLoginLink calls ermite.SessionService.CreateLoginLink.
@@ -100,6 +128,16 @@ func (c *sessionServiceClient) SignOutEverywhere(ctx context.Context, req *conne
100 return c.signOutEverywhere.CallUnary(ctx, req)128 return c.signOutEverywhere.CallUnary(ctx, req)
101 }129 }
102 130
131+// ListPasskeys calls ermite.SessionService.ListPasskeys.
132+func (c *sessionServiceClient) ListPasskeys(ctx context.Context, req *connect.Request[ermite.ListPasskeysRequest]) (*connect.Response[ermite.ListPasskeysResponse], error) {
133+ return c.listPasskeys.CallUnary(ctx, req)
134+}
135+
136+// RemovePasskey calls ermite.SessionService.RemovePasskey.
137+func (c *sessionServiceClient) RemovePasskey(ctx context.Context, req *connect.Request[ermite.RemovePasskeyRequest]) (*connect.Response[ermite.RemovePasskeyResponse], error) {
138+ return c.removePasskey.CallUnary(ctx, req)
139+}
140+
103 // SessionServiceHandler is an implementation of the ermite.SessionService service.141 // SessionServiceHandler is an implementation of the ermite.SessionService service.
104 type SessionServiceHandler interface {142 type SessionServiceHandler interface {
105 // CreateLoginLink creates a one-time link that signs the owner's browser143 // CreateLoginLink creates a one-time link that signs the owner's browser
@@ -113,6 +151,14 @@ type SessionServiceHandler interface {
113 // other devices. Pending login links are not affected. Tokens are not151 // other devices. Pending login links are not affected. Tokens are not
114 // affected either: revoke them with TokenService.152 // affected either: revoke them with TokenService.
115 SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error)153 SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error)
154+ // ListPasskeys lists the owner's registered passkeys, oldest first. Fails
155+ // with failed_precondition when the server has passkeys disabled.
156+ ListPasskeys(context.Context, *connect.Request[ermite.ListPasskeysRequest]) (*connect.Response[ermite.ListPasskeysResponse], error)
157+ // RemovePasskey removes one passkey. It also signs out every browser
158+ // session, on every device, like SignOutEverywhere. Fails with not_found
159+ // for an unknown id, invalid_argument for an id that is not base64url, and
160+ // failed_precondition when the server has passkeys disabled.
161+ RemovePasskey(context.Context, *connect.Request[ermite.RemovePasskeyRequest]) (*connect.Response[ermite.RemovePasskeyResponse], error)
116 }162 }
117 163
118 // NewSessionServiceHandler builds an HTTP handler from the service implementation. It returns the164 // NewSessionServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -134,12 +180,28 @@ func NewSessionServiceHandler(svc SessionServiceHandler, opts ...connect.Handler
134 connect.WithSchema(sessionServiceMethods.ByName("SignOutEverywhere")),180 connect.WithSchema(sessionServiceMethods.ByName("SignOutEverywhere")),
135 connect.WithHandlerOptions(opts...),181 connect.WithHandlerOptions(opts...),
136 )182 )
183+ sessionServiceListPasskeysHandler := connect.NewUnaryHandler(
184+ SessionServiceListPasskeysProcedure,
185+ svc.ListPasskeys,
186+ connect.WithSchema(sessionServiceMethods.ByName("ListPasskeys")),
187+ connect.WithHandlerOptions(opts...),
188+ )
189+ sessionServiceRemovePasskeyHandler := connect.NewUnaryHandler(
190+ SessionServiceRemovePasskeyProcedure,
191+ svc.RemovePasskey,
192+ connect.WithSchema(sessionServiceMethods.ByName("RemovePasskey")),
193+ connect.WithHandlerOptions(opts...),
194+ )
137 return "/ermite.SessionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {195 return "/ermite.SessionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
138 switch r.URL.Path {196 switch r.URL.Path {
139 case SessionServiceCreateLoginLinkProcedure:197 case SessionServiceCreateLoginLinkProcedure:
140 sessionServiceCreateLoginLinkHandler.ServeHTTP(w, r)198 sessionServiceCreateLoginLinkHandler.ServeHTTP(w, r)
141 case SessionServiceSignOutEverywhereProcedure:199 case SessionServiceSignOutEverywhereProcedure:
142 sessionServiceSignOutEverywhereHandler.ServeHTTP(w, r)200 sessionServiceSignOutEverywhereHandler.ServeHTTP(w, r)
201+ case SessionServiceListPasskeysProcedure:
202+ sessionServiceListPasskeysHandler.ServeHTTP(w, r)
203+ case SessionServiceRemovePasskeyProcedure:
204+ sessionServiceRemovePasskeyHandler.ServeHTTP(w, r)
143 default:205 default:
144 http.NotFound(w, r)206 http.NotFound(w, r)
145 }207 }
@@ -156,3 +218,11 @@ func (UnimplementedSessionServiceHandler) CreateLoginLink(context.Context, *conn
156 func (UnimplementedSessionServiceHandler) SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error) {218 func (UnimplementedSessionServiceHandler) SignOutEverywhere(context.Context, *connect.Request[ermite.SignOutEverywhereRequest]) (*connect.Response[ermite.SignOutEverywhereResponse], error) {
157 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.SessionService.SignOutEverywhere is not implemented"))219 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.SessionService.SignOutEverywhere is not implemented"))
158 }220 }
221+
222+func (UnimplementedSessionServiceHandler) ListPasskeys(context.Context, *connect.Request[ermite.ListPasskeysRequest]) (*connect.Response[ermite.ListPasskeysResponse], error) {
223+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.SessionService.ListPasskeys is not implemented"))
224+}
225+
226+func (UnimplementedSessionServiceHandler) RemovePasskey(context.Context, *connect.Request[ermite.RemovePasskeyRequest]) (*connect.Response[ermite.RemovePasskeyResponse], error) {
227+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.SessionService.RemovePasskey is not implemented"))
228+}
M gen/ermite/session.pb.go +296 -15
@@ -192,6 +192,259 @@ func (*SignOutEverywhereResponse) Descriptor() ([]byte, []int) {
192192 return file_ermite_session_proto_rawDescGZIP(), []int{3}
193193 }
194194
195+// Passkey is a WebAuthn credential that can sign the owner in. It never
196+// carries key material.
197+type Passkey struct {
198+ state protoimpl.MessageState `protogen:"open.v1"`
199+ // Id is the credential id, base64url without padding. Pass it to
200+ // RemovePasskey.
201+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
202+ // Name is the label the owner gave the passkey when registering it.
203+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
204+ // CreatedAt is when the passkey was registered.
205+ CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
206+ // LastUsedAt is when the passkey last signed the owner in. It is unset
207+ // when the passkey was never used.
208+ LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"`
209+ // Synced is true when the authenticator reports the credential as backed
210+ // up (the backup state flag), for example a passkey synced by a platform or
211+ // a password manager.
212+ Synced bool `protobuf:"varint,5,opt,name=synced,proto3" json:"synced,omitempty"`
213+ unknownFields protoimpl.UnknownFields
214+ sizeCache protoimpl.SizeCache
215+}
216+
217+func (x *Passkey) Reset() {
218+ *x = Passkey{}
219+ mi := &file_ermite_session_proto_msgTypes[4]
220+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
221+ ms.StoreMessageInfo(mi)
222+}
223+
224+func (x *Passkey) String() string {
225+ return protoimpl.X.MessageStringOf(x)
226+}
227+
228+func (*Passkey) ProtoMessage() {}
229+
230+func (x *Passkey) ProtoReflect() protoreflect.Message {
231+ mi := &file_ermite_session_proto_msgTypes[4]
232+ if x != nil {
233+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
234+ if ms.LoadMessageInfo() == nil {
235+ ms.StoreMessageInfo(mi)
236+ }
237+ return ms
238+ }
239+ return mi.MessageOf(x)
240+}
241+
242+// Deprecated: Use Passkey.ProtoReflect.Descriptor instead.
243+func (*Passkey) Descriptor() ([]byte, []int) {
244+ return file_ermite_session_proto_rawDescGZIP(), []int{4}
245+}
246+
247+func (x *Passkey) GetId() string {
248+ if x != nil {
249+ return x.Id
250+ }
251+ return ""
252+}
253+
254+func (x *Passkey) GetName() string {
255+ if x != nil {
256+ return x.Name
257+ }
258+ return ""
259+}
260+
261+func (x *Passkey) GetCreatedAt() *timestamppb.Timestamp {
262+ if x != nil {
263+ return x.CreatedAt
264+ }
265+ return nil
266+}
267+
268+func (x *Passkey) GetLastUsedAt() *timestamppb.Timestamp {
269+ if x != nil {
270+ return x.LastUsedAt
271+ }
272+ return nil
273+}
274+
275+func (x *Passkey) GetSynced() bool {
276+ if x != nil {
277+ return x.Synced
278+ }
279+ return false
280+}
281+
282+// ListPasskeysRequest is empty.
283+type ListPasskeysRequest struct {
284+ state protoimpl.MessageState `protogen:"open.v1"`
285+ unknownFields protoimpl.UnknownFields
286+ sizeCache protoimpl.SizeCache
287+}
288+
289+func (x *ListPasskeysRequest) Reset() {
290+ *x = ListPasskeysRequest{}
291+ mi := &file_ermite_session_proto_msgTypes[5]
292+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
293+ ms.StoreMessageInfo(mi)
294+}
295+
296+func (x *ListPasskeysRequest) String() string {
297+ return protoimpl.X.MessageStringOf(x)
298+}
299+
300+func (*ListPasskeysRequest) ProtoMessage() {}
301+
302+func (x *ListPasskeysRequest) ProtoReflect() protoreflect.Message {
303+ mi := &file_ermite_session_proto_msgTypes[5]
304+ if x != nil {
305+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
306+ if ms.LoadMessageInfo() == nil {
307+ ms.StoreMessageInfo(mi)
308+ }
309+ return ms
310+ }
311+ return mi.MessageOf(x)
312+}
313+
314+// Deprecated: Use ListPasskeysRequest.ProtoReflect.Descriptor instead.
315+func (*ListPasskeysRequest) Descriptor() ([]byte, []int) {
316+ return file_ermite_session_proto_rawDescGZIP(), []int{5}
317+}
318+
319+// ListPasskeysResponse holds the passkeys.
320+type ListPasskeysResponse struct {
321+ state protoimpl.MessageState `protogen:"open.v1"`
322+ // Passkeys are the registered passkeys, oldest first.
323+ Passkeys []*Passkey `protobuf:"bytes,1,rep,name=passkeys,proto3" json:"passkeys,omitempty"`
324+ unknownFields protoimpl.UnknownFields
325+ sizeCache protoimpl.SizeCache
326+}
327+
328+func (x *ListPasskeysResponse) Reset() {
329+ *x = ListPasskeysResponse{}
330+ mi := &file_ermite_session_proto_msgTypes[6]
331+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
332+ ms.StoreMessageInfo(mi)
333+}
334+
335+func (x *ListPasskeysResponse) String() string {
336+ return protoimpl.X.MessageStringOf(x)
337+}
338+
339+func (*ListPasskeysResponse) ProtoMessage() {}
340+
341+func (x *ListPasskeysResponse) ProtoReflect() protoreflect.Message {
342+ mi := &file_ermite_session_proto_msgTypes[6]
343+ if x != nil {
344+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
345+ if ms.LoadMessageInfo() == nil {
346+ ms.StoreMessageInfo(mi)
347+ }
348+ return ms
349+ }
350+ return mi.MessageOf(x)
351+}
352+
353+// Deprecated: Use ListPasskeysResponse.ProtoReflect.Descriptor instead.
354+func (*ListPasskeysResponse) Descriptor() ([]byte, []int) {
355+ return file_ermite_session_proto_rawDescGZIP(), []int{6}
356+}
357+
358+func (x *ListPasskeysResponse) GetPasskeys() []*Passkey {
359+ if x != nil {
360+ return x.Passkeys
361+ }
362+ return nil
363+}
364+
365+// RemovePasskeyRequest names the passkey to remove.
366+type RemovePasskeyRequest struct {
367+ state protoimpl.MessageState `protogen:"open.v1"`
368+ // Id is the credential id from Passkey.id.
369+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
370+ unknownFields protoimpl.UnknownFields
371+ sizeCache protoimpl.SizeCache
372+}
373+
374+func (x *RemovePasskeyRequest) Reset() {
375+ *x = RemovePasskeyRequest{}
376+ mi := &file_ermite_session_proto_msgTypes[7]
377+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
378+ ms.StoreMessageInfo(mi)
379+}
380+
381+func (x *RemovePasskeyRequest) String() string {
382+ return protoimpl.X.MessageStringOf(x)
383+}
384+
385+func (*RemovePasskeyRequest) ProtoMessage() {}
386+
387+func (x *RemovePasskeyRequest) ProtoReflect() protoreflect.Message {
388+ mi := &file_ermite_session_proto_msgTypes[7]
389+ if x != nil {
390+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
391+ if ms.LoadMessageInfo() == nil {
392+ ms.StoreMessageInfo(mi)
393+ }
394+ return ms
395+ }
396+ return mi.MessageOf(x)
397+}
398+
399+// Deprecated: Use RemovePasskeyRequest.ProtoReflect.Descriptor instead.
400+func (*RemovePasskeyRequest) Descriptor() ([]byte, []int) {
401+ return file_ermite_session_proto_rawDescGZIP(), []int{7}
402+}
403+
404+func (x *RemovePasskeyRequest) GetId() string {
405+ if x != nil {
406+ return x.Id
407+ }
408+ return ""
409+}
410+
411+// RemovePasskeyResponse is empty.
412+type RemovePasskeyResponse struct {
413+ state protoimpl.MessageState `protogen:"open.v1"`
414+ unknownFields protoimpl.UnknownFields
415+ sizeCache protoimpl.SizeCache
416+}
417+
418+func (x *RemovePasskeyResponse) Reset() {
419+ *x = RemovePasskeyResponse{}
420+ mi := &file_ermite_session_proto_msgTypes[8]
421+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
422+ ms.StoreMessageInfo(mi)
423+}
424+
425+func (x *RemovePasskeyResponse) String() string {
426+ return protoimpl.X.MessageStringOf(x)
427+}
428+
429+func (*RemovePasskeyResponse) ProtoMessage() {}
430+
431+func (x *RemovePasskeyResponse) ProtoReflect() protoreflect.Message {
432+ mi := &file_ermite_session_proto_msgTypes[8]
433+ if x != nil {
434+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
435+ if ms.LoadMessageInfo() == nil {
436+ ms.StoreMessageInfo(mi)
437+ }
438+ return ms
439+ }
440+ return mi.MessageOf(x)
441+}
442+
443+// Deprecated: Use RemovePasskeyResponse.ProtoReflect.Descriptor instead.
444+func (*RemovePasskeyResponse) Descriptor() ([]byte, []int) {
445+ return file_ermite_session_proto_rawDescGZIP(), []int{8}
446+}
447+
195448 var File_ermite_session_proto protoreflect.FileDescriptor
196449
197450 const file_ermite_session_proto_rawDesc = "" +
@@ -203,10 +456,26 @@ const file_ermite_session_proto_rawDesc = "" +
203456 "\n" +
204457 "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\x1a\n" +
205458 "\x18SignOutEverywhereRequest\"\x1b\n" +
206- "\x19SignOutEverywhereResponse2\xbe\x01\n" +
459+ "\x19SignOutEverywhereResponse\"\xbe\x01\n" +
460+ "\aPasskey\x12\x0e\n" +
461+ "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
462+ "\x04name\x18\x02 \x01(\tR\x04name\x129\n" +
463+ "\n" +
464+ "created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" +
465+ "\flast_used_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
466+ "lastUsedAt\x12\x16\n" +
467+ "\x06synced\x18\x05 \x01(\bR\x06synced\"\x15\n" +
468+ "\x13ListPasskeysRequest\"C\n" +
469+ "\x14ListPasskeysResponse\x12+\n" +
470+ "\bpasskeys\x18\x01 \x03(\v2\x0f.ermite.PasskeyR\bpasskeys\"&\n" +
471+ "\x14RemovePasskeyRequest\x12\x0e\n" +
472+ "\x02id\x18\x01 \x01(\tR\x02id\"\x17\n" +
473+ "\x15RemovePasskeyResponse2\xd7\x02\n" +
207474 "\x0eSessionService\x12R\n" +
208475 "\x0fCreateLoginLink\x12\x1e.ermite.CreateLoginLinkRequest\x1a\x1f.ermite.CreateLoginLinkResponse\x12X\n" +
209- "\x11SignOutEverywhere\x12 .ermite.SignOutEverywhereRequest\x1a!.ermite.SignOutEverywhereResponseBs\n" +
476+ "\x11SignOutEverywhere\x12 .ermite.SignOutEverywhereRequest\x1a!.ermite.SignOutEverywhereResponse\x12I\n" +
477+ "\fListPasskeys\x12\x1b.ermite.ListPasskeysRequest\x1a\x1c.ermite.ListPasskeysResponse\x12L\n" +
478+ "\rRemovePasskey\x12\x1c.ermite.RemovePasskeyRequest\x1a\x1d.ermite.RemovePasskeyResponseBs\n" +
210479 "\n" +
211480 "com.ermiteB\fSessionProtoP\x01Z\x1fbnema.dev/ermite-api/gen/ermite\xa2\x02\x03EXX\xaa\x02\x06Ermite\xca\x02\x06Ermite\xe2\x02\x12Ermite\\GPBMetadata\xea\x02\x06Ermiteb\x06proto3"
212481
@@ -222,25 +491,37 @@ func file_ermite_session_proto_rawDescGZIP() []byte {
222491 return file_ermite_session_proto_rawDescData
223492 }
224493
225-var file_ermite_session_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
494+var file_ermite_session_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
226495 var file_ermite_session_proto_goTypes = []any{
227496 (*CreateLoginLinkRequest)(nil), // 0: ermite.CreateLoginLinkRequest
228497 (*CreateLoginLinkResponse)(nil), // 1: ermite.CreateLoginLinkResponse
229498 (*SignOutEverywhereRequest)(nil), // 2: ermite.SignOutEverywhereRequest
230499 (*SignOutEverywhereResponse)(nil), // 3: ermite.SignOutEverywhereResponse
231- (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
500+ (*Passkey)(nil), // 4: ermite.Passkey
501+ (*ListPasskeysRequest)(nil), // 5: ermite.ListPasskeysRequest
502+ (*ListPasskeysResponse)(nil), // 6: ermite.ListPasskeysResponse
503+ (*RemovePasskeyRequest)(nil), // 7: ermite.RemovePasskeyRequest
504+ (*RemovePasskeyResponse)(nil), // 8: ermite.RemovePasskeyResponse
505+ (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
232506 }
233507 var file_ermite_session_proto_depIdxs = []int32{
234- 4, // 0: ermite.CreateLoginLinkResponse.expires_at:type_name -> google.protobuf.Timestamp
235- 0, // 1: ermite.SessionService.CreateLoginLink:input_type -> ermite.CreateLoginLinkRequest
236- 2, // 2: ermite.SessionService.SignOutEverywhere:input_type -> ermite.SignOutEverywhereRequest
237- 1, // 3: ermite.SessionService.CreateLoginLink:output_type -> ermite.CreateLoginLinkResponse
238- 3, // 4: ermite.SessionService.SignOutEverywhere:output_type -> ermite.SignOutEverywhereResponse
239- 3, // [3:5] is the sub-list for method output_type
240- 1, // [1:3] is the sub-list for method input_type
241- 1, // [1:1] is the sub-list for extension type_name
242- 1, // [1:1] is the sub-list for extension extendee
243- 0, // [0:1] is the sub-list for field type_name
508+ 9, // 0: ermite.CreateLoginLinkResponse.expires_at:type_name -> google.protobuf.Timestamp
509+ 9, // 1: ermite.Passkey.created_at:type_name -> google.protobuf.Timestamp
510+ 9, // 2: ermite.Passkey.last_used_at:type_name -> google.protobuf.Timestamp
511+ 4, // 3: ermite.ListPasskeysResponse.passkeys:type_name -> ermite.Passkey
512+ 0, // 4: ermite.SessionService.CreateLoginLink:input_type -> ermite.CreateLoginLinkRequest
513+ 2, // 5: ermite.SessionService.SignOutEverywhere:input_type -> ermite.SignOutEverywhereRequest
514+ 5, // 6: ermite.SessionService.ListPasskeys:input_type -> ermite.ListPasskeysRequest
515+ 7, // 7: ermite.SessionService.RemovePasskey:input_type -> ermite.RemovePasskeyRequest
516+ 1, // 8: ermite.SessionService.CreateLoginLink:output_type -> ermite.CreateLoginLinkResponse
517+ 3, // 9: ermite.SessionService.SignOutEverywhere:output_type -> ermite.SignOutEverywhereResponse
518+ 6, // 10: ermite.SessionService.ListPasskeys:output_type -> ermite.ListPasskeysResponse
519+ 8, // 11: ermite.SessionService.RemovePasskey:output_type -> ermite.RemovePasskeyResponse
520+ 8, // [8:12] is the sub-list for method output_type
521+ 4, // [4:8] is the sub-list for method input_type
522+ 4, // [4:4] is the sub-list for extension type_name
523+ 4, // [4:4] is the sub-list for extension extendee
524+ 0, // [0:4] is the sub-list for field type_name
244525 }
245526
246527 func init() { file_ermite_session_proto_init() }
@@ -254,7 +535,7 @@ func file_ermite_session_proto_init() {
254535 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
255536 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_session_proto_rawDesc), len(file_ermite_session_proto_rawDesc)),
256537 NumEnums: 0,
257- NumMessages: 4,
538+ NumMessages: 9,
258539 NumExtensions: 0,
259540 NumServices: 1,
260541 },
@@ -192,6 +192,259 @@ func (*SignOutEverywhereResponse) Descriptor() ([]byte, []int) {
192 return file_ermite_session_proto_rawDescGZIP(), []int{3}192 return file_ermite_session_proto_rawDescGZIP(), []int{3}
193 }193 }
194 194
195+// Passkey is a WebAuthn credential that can sign the owner in. It never
196+// carries key material.
197+type Passkey struct {
198+ state protoimpl.MessageState `protogen:"open.v1"`
199+ // Id is the credential id, base64url without padding. Pass it to
200+ // RemovePasskey.
201+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
202+ // Name is the label the owner gave the passkey when registering it.
203+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
204+ // CreatedAt is when the passkey was registered.
205+ CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
206+ // LastUsedAt is when the passkey last signed the owner in. It is unset
207+ // when the passkey was never used.
208+ LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"`
209+ // Synced is true when the authenticator reports the credential as backed
210+ // up (the backup state flag), for example a passkey synced by a platform or
211+ // a password manager.
212+ Synced bool `protobuf:"varint,5,opt,name=synced,proto3" json:"synced,omitempty"`
213+ unknownFields protoimpl.UnknownFields
214+ sizeCache protoimpl.SizeCache
215+}
216+
217+func (x *Passkey) Reset() {
218+ *x = Passkey{}
219+ mi := &file_ermite_session_proto_msgTypes[4]
220+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
221+ ms.StoreMessageInfo(mi)
222+}
223+
224+func (x *Passkey) String() string {
225+ return protoimpl.X.MessageStringOf(x)
226+}
227+
228+func (*Passkey) ProtoMessage() {}
229+
230+func (x *Passkey) ProtoReflect() protoreflect.Message {
231+ mi := &file_ermite_session_proto_msgTypes[4]
232+ if x != nil {
233+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
234+ if ms.LoadMessageInfo() == nil {
235+ ms.StoreMessageInfo(mi)
236+ }
237+ return ms
238+ }
239+ return mi.MessageOf(x)
240+}
241+
242+// Deprecated: Use Passkey.ProtoReflect.Descriptor instead.
243+func (*Passkey) Descriptor() ([]byte, []int) {
244+ return file_ermite_session_proto_rawDescGZIP(), []int{4}
245+}
246+
247+func (x *Passkey) GetId() string {
248+ if x != nil {
249+ return x.Id
250+ }
251+ return ""
252+}
253+
254+func (x *Passkey) GetName() string {
255+ if x != nil {
256+ return x.Name
257+ }
258+ return ""
259+}
260+
261+func (x *Passkey) GetCreatedAt() *timestamppb.Timestamp {
262+ if x != nil {
263+ return x.CreatedAt
264+ }
265+ return nil
266+}
267+
268+func (x *Passkey) GetLastUsedAt() *timestamppb.Timestamp {
269+ if x != nil {
270+ return x.LastUsedAt
271+ }
272+ return nil
273+}
274+
275+func (x *Passkey) GetSynced() bool {
276+ if x != nil {
277+ return x.Synced
278+ }
279+ return false
280+}
281+
282+// ListPasskeysRequest is empty.
283+type ListPasskeysRequest struct {
284+ state protoimpl.MessageState `protogen:"open.v1"`
285+ unknownFields protoimpl.UnknownFields
286+ sizeCache protoimpl.SizeCache
287+}
288+
289+func (x *ListPasskeysRequest) Reset() {
290+ *x = ListPasskeysRequest{}
291+ mi := &file_ermite_session_proto_msgTypes[5]
292+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
293+ ms.StoreMessageInfo(mi)
294+}
295+
296+func (x *ListPasskeysRequest) String() string {
297+ return protoimpl.X.MessageStringOf(x)
298+}
299+
300+func (*ListPasskeysRequest) ProtoMessage() {}
301+
302+func (x *ListPasskeysRequest) ProtoReflect() protoreflect.Message {
303+ mi := &file_ermite_session_proto_msgTypes[5]
304+ if x != nil {
305+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
306+ if ms.LoadMessageInfo() == nil {
307+ ms.StoreMessageInfo(mi)
308+ }
309+ return ms
310+ }
311+ return mi.MessageOf(x)
312+}
313+
314+// Deprecated: Use ListPasskeysRequest.ProtoReflect.Descriptor instead.
315+func (*ListPasskeysRequest) Descriptor() ([]byte, []int) {
316+ return file_ermite_session_proto_rawDescGZIP(), []int{5}
317+}
318+
319+// ListPasskeysResponse holds the passkeys.
320+type ListPasskeysResponse struct {
321+ state protoimpl.MessageState `protogen:"open.v1"`
322+ // Passkeys are the registered passkeys, oldest first.
323+ Passkeys []*Passkey `protobuf:"bytes,1,rep,name=passkeys,proto3" json:"passkeys,omitempty"`
324+ unknownFields protoimpl.UnknownFields
325+ sizeCache protoimpl.SizeCache
326+}
327+
328+func (x *ListPasskeysResponse) Reset() {
329+ *x = ListPasskeysResponse{}
330+ mi := &file_ermite_session_proto_msgTypes[6]
331+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
332+ ms.StoreMessageInfo(mi)
333+}
334+
335+func (x *ListPasskeysResponse) String() string {
336+ return protoimpl.X.MessageStringOf(x)
337+}
338+
339+func (*ListPasskeysResponse) ProtoMessage() {}
340+
341+func (x *ListPasskeysResponse) ProtoReflect() protoreflect.Message {
342+ mi := &file_ermite_session_proto_msgTypes[6]
343+ if x != nil {
344+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
345+ if ms.LoadMessageInfo() == nil {
346+ ms.StoreMessageInfo(mi)
347+ }
348+ return ms
349+ }
350+ return mi.MessageOf(x)
351+}
352+
353+// Deprecated: Use ListPasskeysResponse.ProtoReflect.Descriptor instead.
354+func (*ListPasskeysResponse) Descriptor() ([]byte, []int) {
355+ return file_ermite_session_proto_rawDescGZIP(), []int{6}
356+}
357+
358+func (x *ListPasskeysResponse) GetPasskeys() []*Passkey {
359+ if x != nil {
360+ return x.Passkeys
361+ }
362+ return nil
363+}
364+
365+// RemovePasskeyRequest names the passkey to remove.
366+type RemovePasskeyRequest struct {
367+ state protoimpl.MessageState `protogen:"open.v1"`
368+ // Id is the credential id from Passkey.id.
369+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
370+ unknownFields protoimpl.UnknownFields
371+ sizeCache protoimpl.SizeCache
372+}
373+
374+func (x *RemovePasskeyRequest) Reset() {
375+ *x = RemovePasskeyRequest{}
376+ mi := &file_ermite_session_proto_msgTypes[7]
377+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
378+ ms.StoreMessageInfo(mi)
379+}
380+
381+func (x *RemovePasskeyRequest) String() string {
382+ return protoimpl.X.MessageStringOf(x)
383+}
384+
385+func (*RemovePasskeyRequest) ProtoMessage() {}
386+
387+func (x *RemovePasskeyRequest) ProtoReflect() protoreflect.Message {
388+ mi := &file_ermite_session_proto_msgTypes[7]
389+ if x != nil {
390+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
391+ if ms.LoadMessageInfo() == nil {
392+ ms.StoreMessageInfo(mi)
393+ }
394+ return ms
395+ }
396+ return mi.MessageOf(x)
397+}
398+
399+// Deprecated: Use RemovePasskeyRequest.ProtoReflect.Descriptor instead.
400+func (*RemovePasskeyRequest) Descriptor() ([]byte, []int) {
401+ return file_ermite_session_proto_rawDescGZIP(), []int{7}
402+}
403+
404+func (x *RemovePasskeyRequest) GetId() string {
405+ if x != nil {
406+ return x.Id
407+ }
408+ return ""
409+}
410+
411+// RemovePasskeyResponse is empty.
412+type RemovePasskeyResponse struct {
413+ state protoimpl.MessageState `protogen:"open.v1"`
414+ unknownFields protoimpl.UnknownFields
415+ sizeCache protoimpl.SizeCache
416+}
417+
418+func (x *RemovePasskeyResponse) Reset() {
419+ *x = RemovePasskeyResponse{}
420+ mi := &file_ermite_session_proto_msgTypes[8]
421+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
422+ ms.StoreMessageInfo(mi)
423+}
424+
425+func (x *RemovePasskeyResponse) String() string {
426+ return protoimpl.X.MessageStringOf(x)
427+}
428+
429+func (*RemovePasskeyResponse) ProtoMessage() {}
430+
431+func (x *RemovePasskeyResponse) ProtoReflect() protoreflect.Message {
432+ mi := &file_ermite_session_proto_msgTypes[8]
433+ if x != nil {
434+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
435+ if ms.LoadMessageInfo() == nil {
436+ ms.StoreMessageInfo(mi)
437+ }
438+ return ms
439+ }
440+ return mi.MessageOf(x)
441+}
442+
443+// Deprecated: Use RemovePasskeyResponse.ProtoReflect.Descriptor instead.
444+func (*RemovePasskeyResponse) Descriptor() ([]byte, []int) {
445+ return file_ermite_session_proto_rawDescGZIP(), []int{8}
446+}
447+
195 var File_ermite_session_proto protoreflect.FileDescriptor448 var File_ermite_session_proto protoreflect.FileDescriptor
196 449
197 const file_ermite_session_proto_rawDesc = "" +450 const file_ermite_session_proto_rawDesc = "" +
@@ -203,10 +456,26 @@ const file_ermite_session_proto_rawDesc = "" +
203 "\n" +456 "\n" +
204 "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\x1a\n" +457 "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\x1a\n" +
205 "\x18SignOutEverywhereRequest\"\x1b\n" +458 "\x18SignOutEverywhereRequest\"\x1b\n" +
206- "\x19SignOutEverywhereResponse2\xbe\x01\n" +459+ "\x19SignOutEverywhereResponse\"\xbe\x01\n" +
460+ "\aPasskey\x12\x0e\n" +
461+ "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
462+ "\x04name\x18\x02 \x01(\tR\x04name\x129\n" +
463+ "\n" +
464+ "created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" +
465+ "\flast_used_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
466+ "lastUsedAt\x12\x16\n" +
467+ "\x06synced\x18\x05 \x01(\bR\x06synced\"\x15\n" +
468+ "\x13ListPasskeysRequest\"C\n" +
469+ "\x14ListPasskeysResponse\x12+\n" +
470+ "\bpasskeys\x18\x01 \x03(\v2\x0f.ermite.PasskeyR\bpasskeys\"&\n" +
471+ "\x14RemovePasskeyRequest\x12\x0e\n" +
472+ "\x02id\x18\x01 \x01(\tR\x02id\"\x17\n" +
473+ "\x15RemovePasskeyResponse2\xd7\x02\n" +
207 "\x0eSessionService\x12R\n" +474 "\x0eSessionService\x12R\n" +
208 "\x0fCreateLoginLink\x12\x1e.ermite.CreateLoginLinkRequest\x1a\x1f.ermite.CreateLoginLinkResponse\x12X\n" +475 "\x0fCreateLoginLink\x12\x1e.ermite.CreateLoginLinkRequest\x1a\x1f.ermite.CreateLoginLinkResponse\x12X\n" +
209- "\x11SignOutEverywhere\x12 .ermite.SignOutEverywhereRequest\x1a!.ermite.SignOutEverywhereResponseBs\n" +476+ "\x11SignOutEverywhere\x12 .ermite.SignOutEverywhereRequest\x1a!.ermite.SignOutEverywhereResponse\x12I\n" +
477+ "\fListPasskeys\x12\x1b.ermite.ListPasskeysRequest\x1a\x1c.ermite.ListPasskeysResponse\x12L\n" +
478+ "\rRemovePasskey\x12\x1c.ermite.RemovePasskeyRequest\x1a\x1d.ermite.RemovePasskeyResponseBs\n" +
210 "\n" +479 "\n" +
211 "com.ermiteB\fSessionProtoP\x01Z\x1fbnema.dev/ermite-api/gen/ermite\xa2\x02\x03EXX\xaa\x02\x06Ermite\xca\x02\x06Ermite\xe2\x02\x12Ermite\\GPBMetadata\xea\x02\x06Ermiteb\x06proto3"480 "com.ermiteB\fSessionProtoP\x01Z\x1fbnema.dev/ermite-api/gen/ermite\xa2\x02\x03EXX\xaa\x02\x06Ermite\xca\x02\x06Ermite\xe2\x02\x12Ermite\\GPBMetadata\xea\x02\x06Ermiteb\x06proto3"
212 481
@@ -222,25 +491,37 @@ func file_ermite_session_proto_rawDescGZIP() []byte {
222 return file_ermite_session_proto_rawDescData491 return file_ermite_session_proto_rawDescData
223 }492 }
224 493
225-var file_ermite_session_proto_msgTypes = make([]protoimpl.MessageInfo, 4)494+var file_ermite_session_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
226 var file_ermite_session_proto_goTypes = []any{495 var file_ermite_session_proto_goTypes = []any{
227 (*CreateLoginLinkRequest)(nil), // 0: ermite.CreateLoginLinkRequest496 (*CreateLoginLinkRequest)(nil), // 0: ermite.CreateLoginLinkRequest
228 (*CreateLoginLinkResponse)(nil), // 1: ermite.CreateLoginLinkResponse497 (*CreateLoginLinkResponse)(nil), // 1: ermite.CreateLoginLinkResponse
229 (*SignOutEverywhereRequest)(nil), // 2: ermite.SignOutEverywhereRequest498 (*SignOutEverywhereRequest)(nil), // 2: ermite.SignOutEverywhereRequest
230 (*SignOutEverywhereResponse)(nil), // 3: ermite.SignOutEverywhereResponse499 (*SignOutEverywhereResponse)(nil), // 3: ermite.SignOutEverywhereResponse
231- (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp500+ (*Passkey)(nil), // 4: ermite.Passkey
501+ (*ListPasskeysRequest)(nil), // 5: ermite.ListPasskeysRequest
502+ (*ListPasskeysResponse)(nil), // 6: ermite.ListPasskeysResponse
503+ (*RemovePasskeyRequest)(nil), // 7: ermite.RemovePasskeyRequest
504+ (*RemovePasskeyResponse)(nil), // 8: ermite.RemovePasskeyResponse
505+ (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
232 }506 }
233 var file_ermite_session_proto_depIdxs = []int32{507 var file_ermite_session_proto_depIdxs = []int32{
234- 4, // 0: ermite.CreateLoginLinkResponse.expires_at:type_name -> google.protobuf.Timestamp508+ 9, // 0: ermite.CreateLoginLinkResponse.expires_at:type_name -> google.protobuf.Timestamp
235- 0, // 1: ermite.SessionService.CreateLoginLink:input_type -> ermite.CreateLoginLinkRequest509+ 9, // 1: ermite.Passkey.created_at:type_name -> google.protobuf.Timestamp
236- 2, // 2: ermite.SessionService.SignOutEverywhere:input_type -> ermite.SignOutEverywhereRequest510+ 9, // 2: ermite.Passkey.last_used_at:type_name -> google.protobuf.Timestamp
237- 1, // 3: ermite.SessionService.CreateLoginLink:output_type -> ermite.CreateLoginLinkResponse511+ 4, // 3: ermite.ListPasskeysResponse.passkeys:type_name -> ermite.Passkey
238- 3, // 4: ermite.SessionService.SignOutEverywhere:output_type -> ermite.SignOutEverywhereResponse512+ 0, // 4: ermite.SessionService.CreateLoginLink:input_type -> ermite.CreateLoginLinkRequest
239- 3, // [3:5] is the sub-list for method output_type513+ 2, // 5: ermite.SessionService.SignOutEverywhere:input_type -> ermite.SignOutEverywhereRequest
240- 1, // [1:3] is the sub-list for method input_type514+ 5, // 6: ermite.SessionService.ListPasskeys:input_type -> ermite.ListPasskeysRequest
241- 1, // [1:1] is the sub-list for extension type_name515+ 7, // 7: ermite.SessionService.RemovePasskey:input_type -> ermite.RemovePasskeyRequest
242- 1, // [1:1] is the sub-list for extension extendee516+ 1, // 8: ermite.SessionService.CreateLoginLink:output_type -> ermite.CreateLoginLinkResponse
243- 0, // [0:1] is the sub-list for field type_name517+ 3, // 9: ermite.SessionService.SignOutEverywhere:output_type -> ermite.SignOutEverywhereResponse
518+ 6, // 10: ermite.SessionService.ListPasskeys:output_type -> ermite.ListPasskeysResponse
519+ 8, // 11: ermite.SessionService.RemovePasskey:output_type -> ermite.RemovePasskeyResponse
520+ 8, // [8:12] is the sub-list for method output_type
521+ 4, // [4:8] is the sub-list for method input_type
522+ 4, // [4:4] is the sub-list for extension type_name
523+ 4, // [4:4] is the sub-list for extension extendee
524+ 0, // [0:4] is the sub-list for field type_name
244 }525 }
245 526
246 func init() { file_ermite_session_proto_init() }527 func init() { file_ermite_session_proto_init() }
@@ -254,7 +535,7 @@ func file_ermite_session_proto_init() {
254 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),535 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
255 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_session_proto_rawDesc), len(file_ermite_session_proto_rawDesc)),536 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_session_proto_rawDesc), len(file_ermite_session_proto_rawDesc)),
256 NumEnums: 0,537 NumEnums: 0,
257- NumMessages: 4,538+ NumMessages: 9,
258 NumExtensions: 0,539 NumExtensions: 0,
259 NumServices: 1,540 NumServices: 1,
260 },541 },
M proto/ermite/session.proto +54 -4
@@ -6,9 +6,12 @@ package ermite;
66
77 import "google/protobuf/timestamp.proto";
88
9-// SessionService manages the owner's browser sessions. The browser itself
10-// never calls it: it signs in by opening the link that CreateLoginLink
11-// returns, and signs out by posting to the web's logout route.
9+// SessionService manages the owner's browser sessions and passkeys. The
10+// browser itself never calls it: it signs in by opening the link that
11+// CreateLoginLink returns, or with a passkey on the web's sign-in page, and
12+// signs out by posting to the web's logout route. Registering a passkey and
13+// signing in with one are browser-only ceremonies and are not part of this
14+// API.
1215 //
1316 // Every RPC is owner-only: it needs the root token, or a stored token that
1417 // holds every scope and is not restricted to repositories. Any other caller
@@ -16,7 +19,9 @@ import "google/protobuf/timestamp.proto";
1619 // API accepts bearer tokens only and ignores cookies.
1720 //
1821 // Servers that implement this service list "session" in
19-// GetVersionResponse.features.
22+// GetVersionResponse.features. They also list "passkeys" when passkeys are
23+// available (they need an https public URL with a DNS host name); without it,
24+// ListPasskeys and RemovePasskey fail with failed_precondition.
2025 service SessionService {
2126 // CreateLoginLink creates a one-time link that signs the owner's browser
2227 // in. The link is valid for a short time (about a minute), can be used
@@ -29,6 +34,14 @@ service SessionService {
2934 // other devices. Pending login links are not affected. Tokens are not
3035 // affected either: revoke them with TokenService.
3136 rpc SignOutEverywhere(SignOutEverywhereRequest) returns (SignOutEverywhereResponse);
37+ // ListPasskeys lists the owner's registered passkeys, oldest first. Fails
38+ // with failed_precondition when the server has passkeys disabled.
39+ rpc ListPasskeys(ListPasskeysRequest) returns (ListPasskeysResponse);
40+ // RemovePasskey removes one passkey. It also signs out every browser
41+ // session, on every device, like SignOutEverywhere. Fails with not_found
42+ // for an unknown id, invalid_argument for an id that is not base64url, and
43+ // failed_precondition when the server has passkeys disabled.
44+ rpc RemovePasskey(RemovePasskeyRequest) returns (RemovePasskeyResponse);
3245 }
3346
3447 // CreateLoginLinkRequest is empty.
@@ -49,3 +62,40 @@ message SignOutEverywhereRequest {}
4962
5063 // SignOutEverywhereResponse is empty.
5164 message SignOutEverywhereResponse {}
65+
66+// Passkey is a WebAuthn credential that can sign the owner in. It never
67+// carries key material.
68+message Passkey {
69+ // Id is the credential id, base64url without padding. Pass it to
70+ // RemovePasskey.
71+ string id = 1;
72+ // Name is the label the owner gave the passkey when registering it.
73+ string name = 2;
74+ // CreatedAt is when the passkey was registered.
75+ google.protobuf.Timestamp created_at = 3;
76+ // LastUsedAt is when the passkey last signed the owner in. It is unset
77+ // when the passkey was never used.
78+ google.protobuf.Timestamp last_used_at = 4;
79+ // Synced is true when the authenticator reports the credential as backed
80+ // up (the backup state flag), for example a passkey synced by a platform or
81+ // a password manager.
82+ bool synced = 5;
83+}
84+
85+// ListPasskeysRequest is empty.
86+message ListPasskeysRequest {}
87+
88+// ListPasskeysResponse holds the passkeys.
89+message ListPasskeysResponse {
90+ // Passkeys are the registered passkeys, oldest first.
91+ repeated Passkey passkeys = 1;
92+}
93+
94+// RemovePasskeyRequest names the passkey to remove.
95+message RemovePasskeyRequest {
96+ // Id is the credential id from Passkey.id.
97+ string id = 1;
98+}
99+
100+// RemovePasskeyResponse is empty.
101+message RemovePasskeyResponse {}
@@ -6,9 +6,12 @@ package ermite;
6 6
7 import "google/protobuf/timestamp.proto";7 import "google/protobuf/timestamp.proto";
8 8
9-// SessionService manages the owner's browser sessions. The browser itself9+// SessionService manages the owner's browser sessions and passkeys. The
10-// never calls it: it signs in by opening the link that CreateLoginLink10+// browser itself never calls it: it signs in by opening the link that
11-// returns, and signs out by posting to the web's logout route.11+// CreateLoginLink returns, or with a passkey on the web's sign-in page, and
12+// signs out by posting to the web's logout route. Registering a passkey and
13+// signing in with one are browser-only ceremonies and are not part of this
14+// API.
12 //15 //
13 // Every RPC is owner-only: it needs the root token, or a stored token that16 // Every RPC is owner-only: it needs the root token, or a stored token that
14 // holds every scope and is not restricted to repositories. Any other caller17 // holds every scope and is not restricted to repositories. Any other caller
@@ -16,7 +19,9 @@ import "google/protobuf/timestamp.proto";
16 // API accepts bearer tokens only and ignores cookies.19 // API accepts bearer tokens only and ignores cookies.
17 //20 //
18 // Servers that implement this service list "session" in21 // Servers that implement this service list "session" in
19-// GetVersionResponse.features.22+// GetVersionResponse.features. They also list "passkeys" when passkeys are
23+// available (they need an https public URL with a DNS host name); without it,
24+// ListPasskeys and RemovePasskey fail with failed_precondition.
20 service SessionService {25 service SessionService {
21 // CreateLoginLink creates a one-time link that signs the owner's browser26 // CreateLoginLink creates a one-time link that signs the owner's browser
22 // in. The link is valid for a short time (about a minute), can be used27 // in. The link is valid for a short time (about a minute), can be used
@@ -29,6 +34,14 @@ service SessionService {
29 // other devices. Pending login links are not affected. Tokens are not34 // other devices. Pending login links are not affected. Tokens are not
30 // affected either: revoke them with TokenService.35 // affected either: revoke them with TokenService.
31 rpc SignOutEverywhere(SignOutEverywhereRequest) returns (SignOutEverywhereResponse);36 rpc SignOutEverywhere(SignOutEverywhereRequest) returns (SignOutEverywhereResponse);
37+ // ListPasskeys lists the owner's registered passkeys, oldest first. Fails
38+ // with failed_precondition when the server has passkeys disabled.
39+ rpc ListPasskeys(ListPasskeysRequest) returns (ListPasskeysResponse);
40+ // RemovePasskey removes one passkey. It also signs out every browser
41+ // session, on every device, like SignOutEverywhere. Fails with not_found
42+ // for an unknown id, invalid_argument for an id that is not base64url, and
43+ // failed_precondition when the server has passkeys disabled.
44+ rpc RemovePasskey(RemovePasskeyRequest) returns (RemovePasskeyResponse);
32 }45 }
33 46
34 // CreateLoginLinkRequest is empty.47 // CreateLoginLinkRequest is empty.
@@ -49,3 +62,40 @@ message SignOutEverywhereRequest {}
49 62
50 // SignOutEverywhereResponse is empty.63 // SignOutEverywhereResponse is empty.
51 message SignOutEverywhereResponse {}64 message SignOutEverywhereResponse {}
65+
66+// Passkey is a WebAuthn credential that can sign the owner in. It never
67+// carries key material.
68+message Passkey {
69+ // Id is the credential id, base64url without padding. Pass it to
70+ // RemovePasskey.
71+ string id = 1;
72+ // Name is the label the owner gave the passkey when registering it.
73+ string name = 2;
74+ // CreatedAt is when the passkey was registered.
75+ google.protobuf.Timestamp created_at = 3;
76+ // LastUsedAt is when the passkey last signed the owner in. It is unset
77+ // when the passkey was never used.
78+ google.protobuf.Timestamp last_used_at = 4;
79+ // Synced is true when the authenticator reports the credential as backed
80+ // up (the backup state flag), for example a passkey synced by a platform or
81+ // a password manager.
82+ bool synced = 5;
83+}
84+
85+// ListPasskeysRequest is empty.
86+message ListPasskeysRequest {}
87+
88+// ListPasskeysResponse holds the passkeys.
89+message ListPasskeysResponse {
90+ // Passkeys are the registered passkeys, oldest first.
91+ repeated Passkey passkeys = 1;
92+}
93+
94+// RemovePasskeyRequest names the passkey to remove.
95+message RemovePasskeyRequest {
96+ // Id is the credential id from Passkey.id.
97+ string id = 1;
98+}
99+
100+// RemovePasskeyResponse is empty.
101+message RemovePasskeyResponse {}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 18

↑↓ moveenter openesc close