hosted-workspace-resolve.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
2 days ago
| 1 | /** |
| 2 | * Hosted Hub: resolve which canister partition (X-User-Id) a JWT actor uses. |
| 3 | * Shared by bridge (hosted-context, index/search/sync); workspace semantics in docs/MULTI-VAULT-AND-SCOPED-ACCESS.md. |
| 4 | */ |
| 5 | |
| 6 | /** Roles that participate in workspace delegation (read owner's canister partition). Must match bridge VALID_ROLES. */ |
| 7 | export const HOSTED_VALID_ROLES = new Set(['admin', 'editor', 'viewer', 'evaluator']); |
| 8 | |
| 9 | /** |
| 10 | * @param {object} p |
| 11 | * @param {string} p.actorSub - JWT sub (e.g. google:123) |
| 12 | * @param {string|null|undefined} p.workspaceOwnerId - bridge hub_workspace.owner_user_id |
| 13 | * @param {Record<string, string>} p.storedRoles - hub_roles map |
| 14 | * @param {Set<string>} p.adminUserIdsSet - HUB_ADMIN_USER_IDS |
| 15 | * @returns {{ effective: string, delegate: boolean }} |
| 16 | */ |
| 17 | export function resolveEffectiveCanisterUser({ actorSub, workspaceOwnerId, storedRoles, adminUserIdsSet }) { |
| 18 | if (!actorSub) return { effective: '', delegate: false }; |
| 19 | const owner = |
| 20 | workspaceOwnerId && String(workspaceOwnerId).trim() ? String(workspaceOwnerId).trim() : null; |
| 21 | if (!owner) return { effective: actorSub, delegate: false }; |
| 22 | if (actorSub === owner) return { effective: actorSub, delegate: false }; |
| 23 | const stored = storedRoles && storedRoles[actorSub]; |
| 24 | const inTeamRoles = Boolean(stored && HOSTED_VALID_ROLES.has(stored)); |
| 25 | const envAdmin = Boolean(adminUserIdsSet && adminUserIdsSet.has(actorSub)); |
| 26 | if (inTeamRoles || envAdmin) return { effective: owner, delegate: true }; |
| 27 | return { effective: actorSub, delegate: false }; |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Same semantics as hub/hub_vault_access.mjs getAllowedVaultIds. |
| 32 | * @param {Record<string, string[]>} accessMap |
| 33 | * @param {string} userId |
| 34 | * @returns {string[]} |
| 35 | */ |
| 36 | export function getAllowedVaultIdsFromAccessMap(accessMap, userId) { |
| 37 | const access = accessMap && typeof accessMap === 'object' ? accessMap : {}; |
| 38 | const allowed = access[userId]; |
| 39 | return allowed && Array.isArray(allowed) && allowed.length > 0 ? [...allowed] : ['default']; |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * Same semantics as hub/hub_scope.mjs getScopeForUserVault (object in memory, not file). |
| 44 | * @param {Record<string, Record<string, { projects?: string[], folders?: string[] }>>} scopeMap |
| 45 | * @param {string} userId |
| 46 | * @param {string} vaultId |
| 47 | * @returns {{ projects: string[], folders: string[] } | null} |
| 48 | */ |
| 49 | export function getScopeForUserVaultFromScopeMap(scopeMap, userId, vaultId) { |
| 50 | const vid = vaultId && String(vaultId).trim() ? String(vaultId).trim() : 'default'; |
| 51 | const scope = scopeMap && typeof scopeMap === 'object' ? scopeMap : {}; |
| 52 | const userScope = scope[userId]; |
| 53 | if (!userScope || typeof userScope[vid] !== 'object') return null; |
| 54 | const r = userScope[vid]; |
| 55 | const projects = Array.isArray(r.projects) ? r.projects : []; |
| 56 | const folders = Array.isArray(r.folders) ? r.folders : []; |
| 57 | if (projects.length === 0 && folders.length === 0) return null; |
| 58 | return { projects, folders }; |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Intersect canister vault ids with allowlist; preserve canister order. |
| 63 | * @param {string[]} canisterIds |
| 64 | * @param {string[]} allowedRaw |
| 65 | * @returns {string[]} |
| 66 | */ |
| 67 | export function intersectVaultIds(canisterIds, allowedRaw) { |
| 68 | const allow = new Set(allowedRaw && allowedRaw.length ? allowedRaw : ['default']); |
| 69 | const list = Array.isArray(canisterIds) && canisterIds.length ? canisterIds : ['default']; |
| 70 | return list.filter((id) => allow.has(id)); |
| 71 | } |
| 72 | |
| 73 | /** |
| 74 | * Vault-access map restricts **delegating** team members. Users acting on their **own** canister |
| 75 | * partition (owner or solo, not delegating) get every vault returned by the canister unless they |
| 76 | * have an explicit access row (then that row applies). |
| 77 | * |
| 78 | * @param {{ delegate: boolean, actorUid: string, accessMap: Record<string, string[]>, canisterIds: string[] }} p |
| 79 | * @returns {string[]} |
| 80 | */ |
| 81 | export function resolveAllowedVaultIdsForHostedContext({ delegate, actorUid, accessMap, canisterIds }) { |
| 82 | const access = accessMap && typeof accessMap === 'object' ? accessMap : {}; |
| 83 | const explicit = access[actorUid]; |
| 84 | const explicitList = |
| 85 | explicit && Array.isArray(explicit) && explicit.length > 0 |
| 86 | ? explicit.map((x) => String(x)) |
| 87 | : null; |
| 88 | const ids = Array.isArray(canisterIds) && canisterIds.length ? canisterIds.map(String) : ['default']; |
| 89 | const allowedRaw = delegate ? explicitList ?? ['default'] : explicitList ?? ids; |
| 90 | return intersectVaultIds(ids, allowedRaw); |
| 91 | } |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
2 days ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
2 days ago