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 +1 -1
- package/src/auth.js +16 -1
- package/src/roles.js +21 -5
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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.
|