mikser-io 10.12.0 → 10.13.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": "mikser-io",
3
- "version": "10.12.0",
3
+ "version": "10.13.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/auth.js CHANGED
@@ -271,9 +271,24 @@ export function anyOf(...verifiers) {
271
271
  // credential that DOES declare capabilities is bound by them as well, so
272
272
  // a request's reach is the INTERSECTION of the endpoint's ceiling and the
273
273
  // principal's grant — never the union.
274
+ // `*` is a real wildcard, and this is the ONLY place that decides so.
275
+ //
276
+ // It was already being written in grants — a role configured `['*']` meaning
277
+ // "everything" — and matched nothing, because this was an exact `includes`.
278
+ // The result was the worst available shape: a role whose own summary said it
279
+ // had everything, whose reach printed as empty, which could use api and mcp
280
+ // (those declare no capability, so the check above passes) and could not open
281
+ // the drive (which declares one). Everything about it looked deliberate.
282
+ //
283
+ // Enforced through this function everywhere rather than `.includes` at each
284
+ // gate: drive checked twice and the auth verifier once, all bypassing this,
285
+ // so a wildcard added here alone would have worked on two surfaces out of
286
+ // four — the same partial answer, one layer down.
287
+ export const CAPABILITY_WILDCARD = '*'
288
+
274
289
  export function hasCapability(principal, capability) {
275
290
  if (!capability) return true
276
291
  const caps = principal?.capabilities
277
292
  if (caps == null) return true
278
- return caps.includes(capability)
293
+ return caps.includes(capability) || caps.includes(CAPABILITY_WILDCARD)
279
294
  }
package/src/roles.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { CAPABILITY_WILDCARD } from './auth.js'
1
2
  // What a principal may do, in words rather than in capability strings.
2
3
  //
3
4
  // Enforcement only ever needs the flat list: does this credential carry
@@ -76,11 +77,24 @@ export function capabilityMeaning(capability) {
76
77
  // useful than the capabilities it comes from because it is already in the
77
78
  // vocabulary the person asking uses — a collection, the folder it lives in and
78
79
  // what that folder is for, not a verb.
79
- export function reachOf(capabilities = []) {
80
+ // Every capability any role on this site is granted — what `*` expands to.
81
+ function universeOf(catalogue = {}) {
82
+ return [...new Set(Object.values(catalogue).flat())]
83
+ .filter(c => c !== CAPABILITY_WILDCARD)
84
+ }
85
+
86
+ export function reachOf(capabilities = [], { universe = null } = {}) {
80
87
  const readable = new Map()
81
88
  const writable = new Set()
82
89
  const also = []
83
- for (const capability of capabilities ?? []) {
90
+ // A wildcard means every capability this site defines, so its reach is
91
+ // described that way rather than as the bare `*` — a role that can do
92
+ // everything used to print `writable: []`, which reads as "can do
93
+ // nothing" and was the first sign that the grant was inert.
94
+ const held = (capabilities ?? []).includes(CAPABILITY_WILDCARD) && universe
95
+ ? [...new Set(universe)]
96
+ : (capabilities ?? [])
97
+ for (const capability of held) {
84
98
  const meaning = capabilityMeaning(capability)
85
99
  const resource = meaning?.resource
86
100
  if (!resource?.name) { also.push(capability); continue }
@@ -129,8 +143,9 @@ export function actingRole(held = [], catalogue = {}) {
129
143
  // what makes a handoff possible: it tells an agent who to ask. It reveals
130
144
  // nothing about how to become one, and there is no way to ask for one.
131
145
  export function rolesIn(catalogue = {}, { acting = null, summaries = {} } = {}) {
146
+ const universe = universeOf(catalogue)
132
147
  return Object.entries(catalogue).map(([name, capabilities]) => {
133
- const { writable, readOnly, also } = reachOf(capabilities)
148
+ const { writable, readOnly, also } = reachOf(capabilities, { universe })
134
149
  return {
135
150
  name,
136
151
  ...(name === acting ? { acting: true } : {}),
@@ -159,7 +174,7 @@ export function describeAuthority({ capabilities, roles = [], catalogue = {}, su
159
174
  }
160
175
  }
161
176
  const role = actingRole(roles, catalogue)
162
- const { writable, readOnly } = reachOf(capabilities)
177
+ const { writable, readOnly } = reachOf(capabilities, { universe: universeOf(catalogue) })
163
178
  return {
164
179
  role,
165
180
  // Only when no single role covers the others: the acting authority is
@@ -180,7 +195,8 @@ export function describeAuthority({ capabilities, roles = [], catalogue = {}, su
180
195
  // around it — the correct next step is a person, not another call.
181
196
  export function explainRefusal({ capability, role, target, catalogue = {}, summaries = {} } = {}) {
182
197
  const holders = Object.entries(catalogue)
183
- .filter(([, capabilities]) => (capabilities ?? []).includes(capability))
198
+ .filter(([, capabilities]) => (capabilities ?? []).includes(capability)
199
+ || (capabilities ?? []).includes(CAPABILITY_WILDCARD))
184
200
  .map(([name]) => name)
185
201
  // Trim the summary's own full stop: it is a sentence in its own right and
186
202
  // reads as a typo when a second one lands beside it.