@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.
- package/package.json +1 -1
- package/src/attributes.ts +58 -16
- package/src/common/auth.ts +504 -260
- package/src/errors.ts +10 -0
- package/src/fixtures/common.ts +166 -41
- package/src/fixtures/ids.ts +20 -8
- package/src/fixtures/index.ts +13 -1
- package/src/fixtures/topics.ts +114 -25
- package/src/identifiers.ts +46 -35
- package/src/schemas.ts +18 -3
package/src/identifiers.ts
CHANGED
|
@@ -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,
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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:
|
|
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
|
-
|
|
19
|
-
|
|
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 },
|