@ccmsg/protocol 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,12 +43,14 @@ export const InstanceId = Type.String({
43
43
  });
44
44
  export type InstanceId = Static<typeof InstanceId>;
45
45
 
46
- /** A host as a browser serializes one: lowercase labels, or an address literal
47
- * in its brackets. No uppercase, no userinfo, no empty label and no zone id
48
- * every one of those is either a second spelling of one host or a string no URL
49
- * parser will take. */
50
- const HOST =
51
- "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*|\\[[0-9a-f:.]+\\])";
46
+ /** A host name as a browser serializes one: lowercase labels, no uppercase, no
47
+ * userinfo and no empty label each of those is either a second spelling of
48
+ * one host or a string no URL parser will take. */
49
+ const HOST_NAME = "[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*";
50
+
51
+ /** A host: a name as above, or an address literal in its brackets (no zone id,
52
+ * which no URL parser takes). */
53
+ const HOST = `(?:${HOST_NAME}|\\[[0-9a-f:.]+\\])`;
52
54
 
53
55
  /** A port in range, 1 to 65535. */
54
56
  const PORT =
@@ -58,8 +60,11 @@ const PORT =
58
60
  * browser omits it, so writing it would be a second name for one place. `tail`
59
61
  * closes the pattern: the end of the string for an origin, a path for a base
60
62
  * URL, and it is what the lookaheads read to know a port ended. */
63
+ function port(its: string): string {
64
+ return `(?::(?!${its}(?:/|$))${PORT})?`;
65
+ }
66
+
61
67
  function authority(tail: string): string {
62
- const port = (its: string) => `(?::(?!${its}(?:/|$))${PORT})?`;
63
68
  return `^(?:https://${HOST}${port("443")}|http://${HOST}${port("80")})${tail}`;
64
69
  }
65
70
 
@@ -67,6 +72,14 @@ function authority(tail: string): string {
67
72
  * carries no query or fragment. */
68
73
  const BASE_URL = authority("(?:/[^?#\\s]*)?/$");
69
74
 
75
+ /** An origin a WebAuthn ceremony can be held at: `https` on a host that is a
76
+ * domain, or `http` on one of the loopback names a browser trusts. The
77
+ * lookahead is what keeps an IPv4 literal out, a bracketed IPv6 one being
78
+ * outside `HOST_NAME` already. */
79
+ const CEREMONY_ORIGIN =
80
+ `^(?:https://(?!\\d{1,3}(?:\\.\\d{1,3}){3}(?::|$))${HOST_NAME}${port("443")}` +
81
+ `|http://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])${port("80")})$`;
82
+
70
83
  /** Where an instance is published: the base URL everything it serves hangs
71
84
  * under, ending in a slash and naming no route of its own.
72
85
  *
@@ -96,49 +109,47 @@ const BASE_URL = authority("(?:/[^?#\\s]*)?/$");
96
109
  export const Endpoint = Type.String({ $id: "Endpoint", pattern: BASE_URL });
97
110
  export type Endpoint = Static<typeof Endpoint>;
98
111
 
99
- /** Where the web UI is published: the base URL a person opens it at, ending in
100
- * a slash and naming no route of its own (`https://ui.example/ccmsg/`).
101
- *
102
- * The counterpart of `Endpoint` on the other side of the wire. An endpoint says
103
- * where an instance is dialed; this says where the page doing the dialing came
104
- * from, and one of each is what a credential is made against. Spelled to the
105
- * same rule as an endpoint, path and trailing slash included, because it is the
106
- * same kind of value: a base URL that something is published under.
107
- *
108
- * **What is kept and what is compared are different sizes.** The whole URL is
109
- * kept: it is where a person is sent, what an operator configures, and what
110
- * they read back in a list of their own credentials. Every comparison this
111
- * contract makes is of the origin (`originOf`) or the host (`rpIdOf`), because
112
- * a browser writes neither a path in an `Origin` header nor one in a
113
- * `clientDataJSON` — there is nothing finer on the wire to compare. Two web UIs
114
- * under one origin are therefore one place to everything here. The origin is
115
- * read off the URL where a header has to be matched rather than kept beside it
116
- * as a second field that could disagree. */
117
- export const WebUi = Type.String({ $id: "WebUi", pattern: BASE_URL });
118
- export type WebUi = Static<typeof WebUi>;
119
-
120
112
  /** Where a page was served from: a scheme and an authority and nothing else,
121
113
  * spelled as a browser spells it in the `Origin` header and in a credential's
122
114
  * `clientDataJSON` — no path, no trailing slash.
123
115
  *
116
+ * The one unit this contract holds a page to. A credential names one of these
117
+ * and a token family carries it over; nothing finer exists to name, a browser
118
+ * writing a path into neither header nor `clientDataJSON`. Serving two
119
+ * instances under one origin at different paths is therefore not a shape this
120
+ * contract has: the two would be one place to every check made here. Where that
121
+ * separation is wanted, the hosts are what a browser tells apart.
122
+ *
124
123
  * Apart from `Endpoint` because the two are units of different size and answer
125
- * different questions. An endpoint says which instance a person is admitted to
126
- * and is compared with its path; an origin says which site the page in front of
127
- * them came from, which is all the browser's same-origin rules know about and
128
- * all a page's own script cannot lie about. One site may be the page for many
129
- * endpoints, and one origin may carry many instances, so neither is derivable
130
- * from the other.
124
+ * different questions. An endpoint says where an instance is dialed; an origin
125
+ * says which site the page in front of a person came from, which is all the
126
+ * browser's same-origin rules know about and all a page's own script cannot lie
127
+ * about. One site may be the page for many endpoints, and one origin may carry
128
+ * many instances, so neither is derivable from the other and which instance a
129
+ * person may enter is answered by ownership rather than by either of them.
131
130
  *
132
131
  * Held to the one spelling a browser serializes: a lowercase scheme, a
133
132
  * lowercase host, and a port only where it is not the scheme's own. No
134
133
  * userinfo, no path, no trailing slash, nothing else a URL may carry.
135
134
  *
135
+ * Held to somewhere a WebAuthn ceremony could actually be held, too, which is
136
+ * narrower than what a URL parser takes and is the authenticator's rule rather
137
+ * than this contract's taste. A ceremony needs a secure context, so the scheme
138
+ * is `https` — with `http` on the loopback names browsers treat as trustworthy,
139
+ * which is what makes a page runnable on a development machine. And a relying
140
+ * party is a domain, so the host may not be an address literal: `https://198.51.100.9`
141
+ * is a perfectly good origin that could never hold a passkey. Writing it into
142
+ * the type rather than leaving it to the daemon is what keeps the relying party
143
+ * total over the values a record may carry — an origin no ceremony could run at
144
+ * would be a credential that could never have been made, accepted and
145
+ * replicated before anything noticed.
146
+ *
136
147
  * The narrowness is the point rather than pedantry. Every use of this value is
137
148
  * a whole-string comparison — against an `Origin` header, against a
138
149
  * `clientDataJSON.origin`, against the members of a CORS answer — so a second
139
150
  * spelling of one site would be a record that never matches the site it names,
140
151
  * or an allowed origin that quietly admits nothing. */
141
- export const Origin = Type.String({ $id: "Origin", pattern: authority("$") });
152
+ export const Origin = Type.String({ $id: "Origin", pattern: CEREMONY_ORIGIN });
142
153
  export type Origin = Static<typeof Origin>;
143
154
 
144
155
  /** A delivery-frame id: `<instance id>/<counter>`, numbered by the instance
package/src/schemas.ts CHANGED
@@ -15,8 +15,14 @@ import {
15
15
  AuthRegisterResponse,
16
16
  AuthResolveRequest,
17
17
  AuthResolveResponse,
18
- AuthRotateRequest,
19
- AuthRotateResponse,
18
+ AuthAccountReadRequest,
19
+ AuthAccountReadResponse,
20
+ AuthCredentialRemoveRequest,
21
+ AuthCredentialRemoveResponse,
22
+ AuthOwnershipRemoveRequest,
23
+ AuthOwnershipRemoveResponse,
24
+ AuthEnrollRequest,
25
+ AuthEnrollResponse,
20
26
  } from "./common/auth.ts";
21
27
  import {
22
28
  HelloInstanceRequest,
@@ -151,10 +157,19 @@ export const OP_SCHEMAS: Record<OpName, OpSchemas> = {
151
157
  "auth.challenge": { request: AuthChallengeRequest, response: AuthChallengeResponse },
152
158
  "auth.register": { request: AuthRegisterRequest, response: AuthRegisterResponse },
153
159
  "auth.assert": { request: AuthAssertRequest, response: AuthAssertResponse },
160
+ "auth.enroll": { request: AuthEnrollRequest, response: AuthEnrollResponse },
154
161
  "auth.token.refresh": { request: AuthTokenRefreshRequest, response: AuthTokenRefreshResponse },
155
162
  "auth.extend": { request: AuthExtendRequest, response: AuthExtendResponse },
163
+ "auth.account.read": { request: AuthAccountReadRequest, response: AuthAccountReadResponse },
164
+ "auth.ownership.remove": {
165
+ request: AuthOwnershipRemoveRequest,
166
+ response: AuthOwnershipRemoveResponse,
167
+ },
168
+ "auth.credential.remove": {
169
+ request: AuthCredentialRemoveRequest,
170
+ response: AuthCredentialRemoveResponse,
171
+ },
156
172
  "auth.resolve": { request: AuthResolveRequest, response: AuthResolveResponse },
157
- "auth.rotate": { request: AuthRotateRequest, response: AuthRotateResponse },
158
173
  "message.send": { request: MessageSendRequest, response: MessageSendResponse },
159
174
  "say.post": { request: SayPostRequest, response: SayPostResponse },
160
175
  "say.unread.clear": { request: SayUnreadClearRequest, response: SayUnreadClearResponse },