@ccmsg/protocol 2.3.0 → 2.5.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.3.0",
3
+ "version": "2.5.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
@@ -39,10 +39,12 @@ export interface OpAttributes {
39
39
  * cannot, and they answer before any identity is settled. The route each is
40
40
  * published at belongs to the instance, not here.
41
41
  *
42
- * Being reachable from a page is also what gives the three that settle an
42
+ * Being reachable from a page is also what gives the four that settle an
43
43
  * identity the only headers this contract reads over HTTP: the `Origin` a
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 be one of
44
+ * browser states, held to the enrolment URL's origin on the two that spend one
45
+ * (`auth.register`, `auth.enroll`) and to the credential's or the family's on
46
+ * the two that do not (`auth.assert`, `auth.token.refresh`), and
47
+ * `Sec-Fetch-Site`, which has to be one of
46
48
  * `same-origin`, `same-site` or `cross-site` — the three that say a page made
47
49
  * the call. Anything else fails: `none`, which is a request with no initiator
48
50
  * at all, a header that is absent, and a value this contract does not know.
@@ -70,7 +72,7 @@ const INSTANCE_ONLY = ["instance"] as const;
70
72
  * facts live: authorization, capability gating and forwarding all read it
71
73
  * rather than each carrying their own copy. */
72
74
  export const OP_ATTRIBUTES = {
73
- // --- common: connect, declare the end, and subscribe (15) ---
75
+ // --- common: connect, declare the end, and subscribe (18) ---
74
76
  // A greeting settles what the connection is, and there is one per role: what
75
77
  // each must carry is then the op's own schema rather than a rule read off a
76
78
  // field, and a connection cannot be settled as something neither side meant.
@@ -138,7 +140,7 @@ export const OP_ATTRIBUTES = {
138
140
  errors: ["topic_unknown"],
139
141
  },
140
142
 
141
- // The four ops that authenticate a person are open to every role for the
143
+ // The five ops that authenticate a person are open to every role for the
142
144
  // same reason the greetings are: they run before there is an identity to
143
145
  // check, and what they answer is what settles one. They are `any_instance` because whichever
144
146
  // instance is reached answers — behind a load balancer that is not a choice
@@ -168,6 +170,17 @@ export const OP_ATTRIBUTES = {
168
170
  carrier: "http",
169
171
  errors: ["auth_invalid", "auth_expired", "auth_unknown_issuer"],
170
172
  },
173
+ // Adding an instance to a person who already exists: the same enrolment URL
174
+ // and the same six digits as a registration, answered by an assertion instead
175
+ // of a new credential. Its attributes are the registration's for that reason.
176
+ "auth.enroll": {
177
+ plane: "common",
178
+ roles: ALL_ROLES,
179
+ needs_hello: false,
180
+ locality: "any_instance",
181
+ carrier: "http",
182
+ errors: ["auth_invalid", "auth_expired", "auth_unknown_issuer"],
183
+ },
171
184
  "auth.token.refresh": {
172
185
  plane: "common",
173
186
  roles: ALL_ROLES,
@@ -185,17 +198,44 @@ export const OP_ATTRIBUTES = {
185
198
  locality: "any_instance",
186
199
  errors: ["auth_invalid", "auth_expired"],
187
200
  },
188
- // Between instances: what an issuer alone can answer. `owner_instance` by the
189
- // usual rulethe subject belongs to one instance, and it is reached by
190
- // `to_instance` being that instance's id.
191
- "auth.resolve": {
201
+ // Reads back the caller's own account, which the connection already settled
202
+ // who isso there is nothing further to check and nothing to name.
203
+ // `scope: "role"` because the role is what decides the reply's contents: it
204
+ // is the caller's own user, passkeys and instances and nobody else's.
205
+ "auth.account.read": {
192
206
  plane: "common",
193
- roles: INSTANCE_ONLY,
207
+ roles: USER_ONLY,
194
208
  needs_hello: true,
195
- locality: "owner_instance",
196
- errors: ["auth_invalid", "auth_expired"],
209
+ locality: "any_instance",
210
+ scope: "role",
211
+ errors: [],
212
+ },
213
+ // Letting go of one of the two things a person holds. Both act on the
214
+ // caller's own records and nobody else's, which is what `scope: "role"` says
215
+ // here as it does on the read; what is removed is named, and who it belongs
216
+ // to is the connection. `auth_in_use` is the refusal when the thing named is
217
+ // what the call is being made with.
218
+ "auth.ownership.remove": {
219
+ plane: "common",
220
+ roles: USER_ONLY,
221
+ needs_hello: true,
222
+ locality: "any_instance",
223
+ scope: "role",
224
+ errors: ["not_found", "auth_in_use"],
197
225
  },
198
- "auth.rotate": {
226
+ "auth.credential.remove": {
227
+ plane: "common",
228
+ roles: USER_ONLY,
229
+ needs_hello: true,
230
+ locality: "any_instance",
231
+ scope: "role",
232
+ errors: ["not_found", "auth_in_use"],
233
+ },
234
+ // Between instances: what an issuer alone can answer — an enrolment URL's
235
+ // secret and a challenge that may be spent once. `owner_instance` because
236
+ // both live in one instance's memory, and it is reached by `to_instance`
237
+ // being that instance's id.
238
+ "auth.resolve": {
199
239
  plane: "common",
200
240
  roles: INSTANCE_ONLY,
201
241
  needs_hello: true,