@ccmsg/protocol 2.2.0 → 2.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/protocol",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Wire contract (schema + types + op attribute table) shared by the ccmsg daemon and web UI",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
package/src/attributes.ts CHANGED
@@ -42,10 +42,12 @@ export interface OpAttributes {
42
42
  * Being reachable from a page is also what gives the three that settle an
43
43
  * identity the only headers this contract reads over HTTP: the `Origin` a
44
44
  * browser states, held to the origin of the web UI the credential or the
45
- * registration names, and `Sec-Fetch-Site`, which has to say the call came
46
- * from a page at all a navigation typed into the address bar is not how
47
- * anyone authenticates. A missing header is a failure like a wrong one, and either
48
- * answers `auth_invalid` without saying which. `auth.challenge` is checked
45
+ * registration names, and `Sec-Fetch-Site`, which has to be one of
46
+ * `same-origin`, `same-site` or `cross-site`the three that say a page made
47
+ * the call. Anything else fails: `none`, which is a request with no initiator
48
+ * at all, a header that is absent, and a value this contract does not know.
49
+ * Both headers are read as an allowlist rather than a blocklist, and either
50
+ * failing answers `auth_invalid` without saying which. `auth.challenge` is checked
49
51
  * against neither, having nothing yet to be checked against; what it hands
50
52
  * out is spendable only at its issuer. */
51
53
  readonly carrier?: "http";
@@ -200,8 +200,10 @@ export type AuthRegisterArgs = Static<typeof AuthRegisterArgs>;
200
200
  * the cookie exists to have. That holds however far the page is from the
201
201
  * endpoint: a cookie the page's own site cannot reach is sent from a site it
202
202
  * does not own only as a partitioned one, which keeps a session taken at one
203
- * site from being carried to another the same shape one credential per web UI
204
- * already has. Sending it at all across sites takes a browser that partitions
203
+ * site from being carried to another. That partition is by site, where a
204
+ * credential is by origin, so it is the `Origin` held against this family's
205
+ * `webui` that keeps a session to the one place it was made — the cookie's
206
+ * partition answers for sites and nothing finer. Sending it at all across sites takes a browser that partitions
205
207
  * cookies, which is a premise of this contract rather than a case it
206
208
  * accommodates: one that does not is not an environment this is spoken over. */
207
209
  export const AuthSession = Type.Object(
@@ -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,25 @@ function authority(tail: string): string {
67
72
  * carries no query or fragment. */
68
73
  const BASE_URL = authority("(?:/[^?#\\s]*)?/$");
69
74
 
75
+ /** The path part of a base URL, as above. */
76
+ const BASE_PATH = "(?:/[^?#\\s]*)?/$";
77
+
78
+ /** A base URL a WebAuthn ceremony can actually run at.
79
+ *
80
+ * Narrower than `BASE_URL` on two counts, both of them the authenticator's
81
+ * rules rather than this contract's taste. A ceremony needs a secure context,
82
+ * so the scheme is `https` — with `http` on the loopback names browsers treat
83
+ * as trustworthy, which is what makes a web UI runnable on a development
84
+ * machine. And a relying party is a domain, so the host may not be an address
85
+ * literal: `https://198.51.100.9/` parses fine and could never hold a passkey.
86
+ *
87
+ * Writing it into the type rather than leaving it to the daemon is what keeps
88
+ * `rpIdOf` total over the values a record may carry. A URL that no ceremony can
89
+ * run at would be a credential that could never have been made. */
90
+ const WEBUI_URL =
91
+ `^(?:https://(?!\\d{1,3}(?:\\.\\d{1,3}){3}(?:[:/]))${HOST_NAME}${port("443")}` +
92
+ `|http://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])${port("80")})${BASE_PATH}`;
93
+
70
94
  /** Where an instance is published: the base URL everything it serves hangs
71
95
  * under, ending in a slash and naming no route of its own.
72
96
  *
@@ -103,7 +127,12 @@ export type Endpoint = Static<typeof Endpoint>;
103
127
  * where an instance is dialed; this says where the page doing the dialing came
104
128
  * from, and one of each is what a credential is made against. Spelled to the
105
129
  * 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.
130
+ * same kind of value: a base URL that something is published under — but held
131
+ * to a narrower set of them: `https`, or `http` on a loopback name a browser
132
+ * treats as trustworthy, and never an address literal for a host. Those are the
133
+ * authenticator's conditions, not this contract's taste: a ceremony wants a
134
+ * secure context, and a relying party is a domain. A URL outside them is one no
135
+ * credential could have been made at.
107
136
  *
108
137
  * **What is kept and what is compared are different sizes.** The whole URL is
109
138
  * kept: it is where a person is sent, what an operator configures, and what
@@ -114,7 +143,7 @@ export type Endpoint = Static<typeof Endpoint>;
114
143
  * under one origin are therefore one place to everything here. The origin is
115
144
  * read off the URL where a header has to be matched rather than kept beside it
116
145
  * as a second field that could disagree. */
117
- export const WebUi = Type.String({ $id: "WebUi", pattern: BASE_URL });
146
+ export const WebUi = Type.String({ $id: "WebUi", pattern: WEBUI_URL });
118
147
  export type WebUi = Static<typeof WebUi>;
119
148
 
120
149
  /** Where a page was served from: a scheme and an authority and nothing else,