@aglyn/aglyn 1.0.0-beta.175 → 1.0.0-beta.177
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 +11 -10
- package/src/lib/app-utils/account-acquisition.d.ts +164 -0
- package/src/lib/app-utils/account-acquisition.js +300 -0
- package/src/lib/app-utils/account-acquisition.js.map +1 -0
- package/src/lib/app-utils/first-party-hosts.d.ts +86 -0
- package/src/lib/app-utils/first-party-hosts.js +143 -0
- package/src/lib/app-utils/first-party-hosts.js.map +1 -0
- package/src/lib/app-utils/visitor-consent.d.ts +9 -0
- package/src/lib/app-utils/visitor-consent.js +19 -4
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +9 -0
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +15 -1
- package/src/lib/plugin-manager/feature-plugins.js +19 -5
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-matches.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-person-matches.js +103 -0
- package/src/lib/plugin-manager/plugin-person-matches.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/organization.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Multi-tenant organizations (AGL-233, docs/MULTI_TENANT_FIRESTORE.md):\n * the org is the tenant boundary — one subscription, one workspace\n * subdomain, one isolation subtree. Billing fields mirror `AglynOrgBilling`\n * so the plan/entitlement resolvers work on either doc during the\n * transition; org-keyed billing lands with AGL-237.\n */\n\nimport type { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type {\n OrgCrmInbound,\n OrgCrmSettings,\n OrgEntitlements,\n OrgPlan,\n OrgSeatAddons,\n OrgSubscription,\n} from './org-billing.types'\nimport type {\n AglynDocument,\n HostUid,\n OrgUid,\n ScopeToken,\n UserUid,\n} from './platform.types'\n\nexport type { OrgUid } from './platform.types'\n/** Workspace subdomain label: `{slug}.aglyn.com` (Slack-style). */\nexport type OrgSlug = string\n\n/** Org-wide roles, strongest to weakest. */\nexport type OrgRole = 'owner' | 'admin' | 'editor' | 'viewer'\n\n/**\n * Per-host refinement for editor/viewer members (\"3 of 15 sites\").\n *\n * `author` (AGL-2334) is `editor` MINUS publish: it may create and edit\n * every content document on the site, and it may not make any of it live.\n * It exists because the agency guide sells exactly that role — \"a client who\n * may edit content but not publish\" — and until now the narrowest thing we\n * could offer was `viewer`, which cannot edit at all.\n *\n * It is a HOST role rather than a twelfth org permission key deliberately.\n * Publishing is a set of client-direct Firestore writes, so it is enforced\n * in the security rules, and the rules resolve a host request from the\n * `memberRoles` projection with the one `get()` they already do. An org\n * permission key is on the wrong axis: rules cannot evaluate a custom role\n * without a second denormalized projection, and every publish surface would\n * need a server route it does not have.\n *\n * Ordered between `editor` and `viewer` because that is its strength, but\n * NOTHING may treat this union as ordered — `ORG_ROLE_WEIGHT` exists for the\n * org axis and has no counterpart here on purpose. Host roles are compared by\n * membership in a set (`HOST_CONTENT_WRITE_ROLES`, `HOST_PUBLISH_ROLES`), and\n * an author is not \"a weaker editor\" in a way any single number can express.\n */\nexport type HostAccessRole = 'admin' | 'editor' | 'author' | 'viewer'\n\n/**\n * A per-site permission key a collaborator can carry (AGL-2927, AGL-2984).\n * The host role decides what a collaborator may do to the SITE; these keys\n * decide what else they may do on it. Each is a catalog key a plugin\n * declares with host-role defaults, the same dotted key the org catalog\n * names, so one label serves both rosters.\n */\nexport type HostPermissionKey = string\n\n/**\n * Every host role, as a value — for `where('memberRoles.{uid}', 'in', …)`.\n *\n * `/hosts/{hostId}` is gated per document on `memberRoles.{uid}`, and\n * Firestore refuses to run a LIST that could return a denied document. So\n * every client query over `hosts` must carry this filter, and it must name\n * ALL the roles: a role missing from the array is a site the member owns and\n * cannot see, with no error to explain it (AGL-1145).\n *\n * Built from a `Record` rather than written as an array so that adding a role\n * to the union above fails to COMPILE here. A plain `HostAccessRole[]` would\n * happily stay short, which is the silent half of the bug.\n */\nconst HOST_ACCESS_ROLE_KEYS: Record<HostAccessRole, true> = {\n admin: true,\n editor: true,\n author: true,\n viewer: true,\n}\nexport const HOST_ACCESS_ROLES = Object.keys(\n HOST_ACCESS_ROLE_KEYS,\n) as HostAccessRole[]\n\nexport interface AglynOrganization extends AglynDocument {\n $id: OrgUid\n name?: string\n slug?: OrgSlug\n /** Current owner; ownership can move without re-keying anything. */\n ownerUid?: UserUid\n /**\n * Who CREATED the workspace — stamped once inside `createOrganization`'s\n * transaction and mutated by nothing, `transferOrgOwnership` included\n * (AGL-2265).\n *\n * `ownerUid` moves; this does not, and the difference is the whole point.\n * The free-workspace ceiling counts the UNION of the two, so handing a\n * workspace to an alt account, creating a fourth and taking the first one\n * back is not a way past the limit. Denied to client writes in the rules\n * for the same reason — a client that could clear its own attribution could\n * mint free workspaces without limit.\n *\n * Absent on every org created before AGL-2265 shipped, and every reader\n * must tolerate that: those are counted by `ownerUid` exactly as they\n * always were.\n */\n createdByUid?: UserUid\n /** Directory of the org's hosts (mirrors AglynOrgBilling.hosts). */\n hosts?: Record<HostUid, true>\n\n // Billing (mirrors AglynOrgBilling; source of truth moves here with AGL-237)\n plan?: OrgPlan\n entitlements?: OrgEntitlements\n /**\n * Per-org plugin switchboard (AGL-416): ids of plugins the workspace\n * loads (see plugin-manager/enabled-plugins). Absent = all first-party\n * plugins; always-on ids (base components) and the ids on for every\n * workspace (AI) are unioned in regardless — a site switches those off for\n * itself instead.\n */\n enabledPlugins?: string[]\n seatAddons?: OrgSeatAddons\n stripeCustomerId?: string\n subscription?: OrgSubscription\n suspendedAt?: ITimestamp | null\n /** Staff-internal rationale (AGL-202). NEVER shown to the customer. */\n suspendedReason?: string\n /**\n * Lockdown extensions (AGL-1501) on the shipped AGL-202 carrier — the org\n * scope of the panic button. `suspendedReasonCode` is the enum the notice\n * copy switches on (`security`/`billing`/`maintenance`/`manual`; absent =\n * `manual`), `suspendedMessage` is the CUSTOMER-FACING notice body (unlike\n * `suspendedReason`), and `suspendedUntilMs` is an optional expiry — once\n * it passes, the suspension is inactive with no write needed (maintenance\n * windows end on their own). Plain epoch ms, not a Timestamp, so every\n * cache serialization reads it back unchanged. All written only by\n * /api/admin/lockdown; normalized by `app-utils/lockdown.ts`.\n */\n suspendedReasonCode?: string\n suspendedMessage?: string\n suspendedUntilMs?: number\n erasureRequestedAt?: ITimestamp | null\n /**\n * Scope applied to newly created datasets and media when nobody chooses\n * (AGL-1048). `'org'` — the default and today's behavior — shares them\n * with every site. `'host'` starts them private to the site they were\n * created in, which is what an agency running client sites wants: safe\n * by default rather than safe by discipline.\n *\n * Only meaningful when there IS a site in context. Created from the org\n * Media or Data page there is no host to scope to, so those stay `'org'`\n * either way.\n */\n defaultResourceScope?: 'org' | 'host'\n /** The CRM's organization-wide settings (AGL-2613) — see `OrgCrmSettings`. */\n crm?: OrgCrmSettings\n /** The workspace's email capture address (AGL-2657) — see `OrgCrmInbound`. */\n crmInbound?: OrgCrmInbound\n}\n\n/**\n * `orgs/{orgId}/members/{uid}` — THE authorization doc: rules resolve a\n * request with this single read. Owner/admin span every host; editor and\n * viewer see `hostAccess` (or `allHosts`).\n */\nexport interface AglynOrgMember extends AglynDocument {\n $id: UserUid\n role?: OrgRole\n /**\n * Custom role reference (AGL-243): id of an `orgs/{orgId}/roles` doc\n * whose permission map overrides the org role's defaults.\n */\n roleId?: string\n /** Per-member permission overrides (AGL-243); win over every layer. */\n permissions?: Record<string, boolean>\n /** Org-wide host access shortcut; otherwise `hostAccess` decides. */\n allHosts?: boolean\n hostAccess?: Record<HostUid, HostAccessRole>\n /**\n * Per-site permission overrides for a COLLABORATOR (AGL-2927), keyed by\n * the host the grant is for. Absent keys resolve by the host role's\n * default, so a document written before this field existed reads the same\n * as one written after it. Org-wide members are never consulted here:\n * their org role, custom role and `permissions` map decide.\n */\n hostPermissions?: Record<HostUid, Partial<Record<HostPermissionKey, boolean>>>\n /**\n * Denormalized reach as scope tokens (AGL-1038), so rules can intersect\n * it with a resource's `visibleTo` — they cannot derive it from\n * `hostAccess` because the rules language has no `.map()`. Written only\n * by `syncOrgAuthProjections`; never edit it by hand.\n */\n scopeTokens?: ScopeToken[]\n /**\n * Denormalized THREE-LAYER permission verdict, so rules can honor a custom\n * role they cannot resolve.\n *\n * `roleId` points at another document and `permissions` is only the top\n * layer, so a rule reading either alone answers a different question from\n * the server — and reproducing the precedence in CEL needs a second\n * cross-document get() plus correct handling of a dangling id. This is\n * `resolveOrgPermissions(member, customRole)`, already applied.\n *\n * ⚠️ ABSENT IS NOT EMPTY, and the rules must never treat it as either\n * \"everything allowed\" or \"nothing allowed\". Documents predating this\n * field, and any written by a path that bypasses\n * `syncOrgAuthProjections`, carry no map at all; the rules fall back to\n * the layers they CAN read — the role and the per-member overrides — which\n * is exactly the verdict they gave before this field existed. Written only\n * by `syncOrgAuthProjections` and by `createOrganization`'s inline stamp;\n * never edit it by hand.\n */\n resolvedPermissions?: Record<string, boolean>\n /** Denormalized for member lists without N user lookups. */\n displayName?: string\n email?: string\n invitedBy?: UserUid\n joinedAt?: ITimestamp\n /**\n * Denormalized org suspension flag (AGL-210) so host writes stay a\n * single rules read; maintained by the staff suspension API.\n */\n orgSuspended?: boolean\n}\n\n/** `orgs/{orgId}/invites/{inviteId}` — pending email invites. */\nexport interface AglynOrgInvite extends AglynDocument {\n $id: string\n email?: string\n role?: OrgRole\n allHosts?: boolean\n hostAccess?: Record<HostUid, HostAccessRole>\n invitedBy?: UserUid\n createdAt?: ITimestamp\n acceptedAt?: ITimestamp | null\n acceptedBy?: UserUid\n}\n\n/**\n * `orgSlugs/{slug}` — transactional uniqueness reservation; created and\n * deleted only by the org APIs (Admin SDK), publicly readable so the\n * console can resolve a workspace subdomain client-side.\n */\nexport interface OrgSlugReservation {\n orgId: OrgUid\n /**\n * Set when the previous holder renamed away — the tombstone that keeps old\n * workspace URLs redirecting until somebody else wants the name.\n */\n movedTo?: string\n /**\n * Epoch millis at which a PENDING reservation lapses (AGL-2585).\n *\n * Present only on a workspace created by an owner whose email was not yet\n * verified, which is every password signup: the address is held for them,\n * not granted to them, and the hold ends unless they confirm the address.\n * Absent means granted, and a grant never expires — so every workspace made\n * by a verified owner, and every one that predates this field, is outside\n * the rule entirely.\n *\n * Cleared by `reap-unverified-orgs` once the owner verifies. Public, like\n * the rest of this document, which is why nothing identifying the owner is\n * written beside it.\n */\n reservedUntil?: number\n}\n\n/**\n * `hostIndex/{hostId}` — server-written host → org resolver so the tenant\n * renderer and middleware find a host's org without scanning.\n */\nexport interface HostIndexEntry {\n orgId: OrgUid\n subdomain?: string\n}\n\n/**\n * `users/{uid}/orgs/{orgId}` — reverse index for \"my organizations\",\n * maintained transactionally with the member doc by the membership API.\n */\nexport interface UserOrgMembership extends AglynDocument {\n $id: OrgUid\n role?: OrgRole\n orgName?: string\n slug?: OrgSlug\n /**\n * Mirror of `isOrgWideMember(orgs/{orgId}/members/{uid})` (AGL-1032): does\n * this membership reach the whole org, or only a list of sites?\n *\n * `role` alone cannot answer it — `grantHostAccess` writes `role: 'viewer'`\n * here for a site collaborator, exactly what a genuine org-wide viewer\n * carries, so the console could not tell them apart without a second read\n * of the member doc on every org route. Denormalized because the console\n * navigation guard has to be synchronous: an async answer flashes the org\n * chrome before hiding it.\n *\n * ABSENT means org-wide. Rows predating the mirror carry no flag, and a\n * missing field must never lock a real member out of their own workspace\n * (the same legacy shape `isOrgWideMember` handles). Only an explicit\n * `false` scopes the console — see `isOrgWideMembership`.\n *\n * Navigation only. The Firestore rules are the access boundary (AGL-1026);\n * a stale mirror at worst shows a page whose reads then come back empty.\n */\n orgWide?: boolean\n}\n\n/**\n * `users/{uid}/hostMemberships/{hostId}` — reverse index of the sites a user\n * can reach, mirroring `hosts/{hostId}.memberRoles` (AGL-844). Denormalizes the\n * host name so the site switcher and subdomain→id routing query a user's own\n * sites, ordered and name-prefix-searched, without scanning the `hosts`\n * collection. Admin-SDK-maintained beside `memberRoles`; a best-effort\n * convenience index, never an authorization source (the rules still gate host\n * reads on `memberRoles`). NOT named `hosts` — that would collide with the\n * top-level `hosts` collection for collection-group rules/indexes.\n */\nexport interface UserHostMembership extends AglynDocument {\n $id: HostUid\n orgId?: OrgUid\n subdomain?: string\n displayName?: string\n nameLower?: string\n /**\n * Mirror of the host's `seo.favicon` (AGL-1071). The site switcher renders\n * from this projection rather than the host doc, so without the mirror it\n * showed the generic glyph for every site — the favicon feature worked in\n * the sites list and nowhere else. Absent when the site has none; writers\n * must DELETE it on clear rather than omit it, since the rows are written\n * with `{ merge: true }` and an omitted field would keep the old icon.\n */\n favicon?: string\n role?: HostAccessRole\n updatedAt?: ITimestamp\n}\n"],"names":["HOST_ACCESS_ROLE_KEYS","admin","editor","author","viewer","HOST_ACCESS_ROLES","Object","keys"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;CAMC,GA4DD;;;;;;;;;;;;CAYC,GACD,MAAMA,wBAAsD;IAC1DC,OAAO;IACPC,QAAQ;IACRC,QAAQ;IACRC,QAAQ;AACV;AACA,OAAO,MAAMC,oBAAoBC,OAAOC,IAAI,CAC1CP,uBACmB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/organization.types.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Multi-tenant organizations (AGL-233, docs/MULTI_TENANT_FIRESTORE.md):\n * the org is the tenant boundary — one subscription, one workspace\n * subdomain, one isolation subtree. Billing fields mirror `AglynOrgBilling`\n * so the plan/entitlement resolvers work on either doc during the\n * transition; org-keyed billing lands with AGL-237.\n */\n\nimport type { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type { AccountAcquisition } from '../../app-utils/account-acquisition'\nimport type {\n OrgCrmInbound,\n OrgCrmSettings,\n OrgEntitlements,\n OrgPlan,\n OrgSeatAddons,\n OrgSubscription,\n} from './org-billing.types'\nimport type {\n AglynDocument,\n HostUid,\n OrgUid,\n ScopeToken,\n UserUid,\n} from './platform.types'\n\nexport type { OrgUid } from './platform.types'\n/** Workspace subdomain label: `{slug}.aglyn.com` (Slack-style). */\nexport type OrgSlug = string\n\n/** Org-wide roles, strongest to weakest. */\nexport type OrgRole = 'owner' | 'admin' | 'editor' | 'viewer'\n\n/**\n * Per-host refinement for editor/viewer members (\"3 of 15 sites\").\n *\n * `author` (AGL-2334) is `editor` MINUS publish: it may create and edit\n * every content document on the site, and it may not make any of it live.\n * It exists because the agency guide sells exactly that role — \"a client who\n * may edit content but not publish\" — and until now the narrowest thing we\n * could offer was `viewer`, which cannot edit at all.\n *\n * It is a HOST role rather than a twelfth org permission key deliberately.\n * Publishing is a set of client-direct Firestore writes, so it is enforced\n * in the security rules, and the rules resolve a host request from the\n * `memberRoles` projection with the one `get()` they already do. An org\n * permission key is on the wrong axis: rules cannot evaluate a custom role\n * without a second denormalized projection, and every publish surface would\n * need a server route it does not have.\n *\n * Ordered between `editor` and `viewer` because that is its strength, but\n * NOTHING may treat this union as ordered — `ORG_ROLE_WEIGHT` exists for the\n * org axis and has no counterpart here on purpose. Host roles are compared by\n * membership in a set (`HOST_CONTENT_WRITE_ROLES`, `HOST_PUBLISH_ROLES`), and\n * an author is not \"a weaker editor\" in a way any single number can express.\n */\nexport type HostAccessRole = 'admin' | 'editor' | 'author' | 'viewer'\n\n/**\n * A per-site permission key a collaborator can carry (AGL-2927, AGL-2984).\n * The host role decides what a collaborator may do to the SITE; these keys\n * decide what else they may do on it. Each is a catalog key a plugin\n * declares with host-role defaults, the same dotted key the org catalog\n * names, so one label serves both rosters.\n */\nexport type HostPermissionKey = string\n\n/**\n * Every host role, as a value — for `where('memberRoles.{uid}', 'in', …)`.\n *\n * `/hosts/{hostId}` is gated per document on `memberRoles.{uid}`, and\n * Firestore refuses to run a LIST that could return a denied document. So\n * every client query over `hosts` must carry this filter, and it must name\n * ALL the roles: a role missing from the array is a site the member owns and\n * cannot see, with no error to explain it (AGL-1145).\n *\n * Built from a `Record` rather than written as an array so that adding a role\n * to the union above fails to COMPILE here. A plain `HostAccessRole[]` would\n * happily stay short, which is the silent half of the bug.\n */\nconst HOST_ACCESS_ROLE_KEYS: Record<HostAccessRole, true> = {\n admin: true,\n editor: true,\n author: true,\n viewer: true,\n}\nexport const HOST_ACCESS_ROLES = Object.keys(\n HOST_ACCESS_ROLE_KEYS,\n) as HostAccessRole[]\n\nexport interface AglynOrganization extends AglynDocument {\n $id: OrgUid\n name?: string\n slug?: OrgSlug\n /** Current owner; ownership can move without re-keying anything. */\n ownerUid?: UserUid\n /**\n * Who CREATED the workspace — stamped once inside `createOrganization`'s\n * transaction and mutated by nothing, `transferOrgOwnership` included\n * (AGL-2265).\n *\n * `ownerUid` moves; this does not, and the difference is the whole point.\n * The free-workspace ceiling counts the UNION of the two, so handing a\n * workspace to an alt account, creating a fourth and taking the first one\n * back is not a way past the limit. Denied to client writes in the rules\n * for the same reason — a client that could clear its own attribution could\n * mint free workspaces without limit.\n *\n * Absent on every org created before AGL-2265 shipped, and every reader\n * must tolerate that: those are counted by `ownerUid` exactly as they\n * always were.\n */\n createdByUid?: UserUid\n /**\n * Where the workspace came from (AGL-3289): its creator's acquisition\n * record, copied by `createOrganization` at birth and naming the account it\n * was copied from. Written by nothing else, and denied to client writes on\n * every rules branch, for the reason `createdByUid` is: it is the\n * platform's record about the workspace, not the workspace's about itself.\n */\n acquisition?: AccountAcquisition\n /** Directory of the org's hosts (mirrors AglynOrgBilling.hosts). */\n hosts?: Record<HostUid, true>\n\n // Billing (mirrors AglynOrgBilling; source of truth moves here with AGL-237)\n plan?: OrgPlan\n entitlements?: OrgEntitlements\n /**\n * Per-org plugin switchboard (AGL-416): ids of plugins the workspace\n * loads (see plugin-manager/enabled-plugins). Absent = all first-party\n * plugins; always-on ids (base components) and the ids on for every\n * workspace (AI) are unioned in regardless — a site switches those off for\n * itself instead.\n */\n enabledPlugins?: string[]\n seatAddons?: OrgSeatAddons\n stripeCustomerId?: string\n subscription?: OrgSubscription\n suspendedAt?: ITimestamp | null\n /** Staff-internal rationale (AGL-202). NEVER shown to the customer. */\n suspendedReason?: string\n /**\n * Lockdown extensions (AGL-1501) on the shipped AGL-202 carrier — the org\n * scope of the panic button. `suspendedReasonCode` is the enum the notice\n * copy switches on (`security`/`billing`/`maintenance`/`manual`; absent =\n * `manual`), `suspendedMessage` is the CUSTOMER-FACING notice body (unlike\n * `suspendedReason`), and `suspendedUntilMs` is an optional expiry — once\n * it passes, the suspension is inactive with no write needed (maintenance\n * windows end on their own). Plain epoch ms, not a Timestamp, so every\n * cache serialization reads it back unchanged. All written only by\n * /api/admin/lockdown; normalized by `app-utils/lockdown.ts`.\n */\n suspendedReasonCode?: string\n suspendedMessage?: string\n suspendedUntilMs?: number\n erasureRequestedAt?: ITimestamp | null\n /**\n * Scope applied to newly created datasets and media when nobody chooses\n * (AGL-1048). `'org'` — the default and today's behavior — shares them\n * with every site. `'host'` starts them private to the site they were\n * created in, which is what an agency running client sites wants: safe\n * by default rather than safe by discipline.\n *\n * Only meaningful when there IS a site in context. Created from the org\n * Media or Data page there is no host to scope to, so those stay `'org'`\n * either way.\n */\n defaultResourceScope?: 'org' | 'host'\n /** The CRM's organization-wide settings (AGL-2613) — see `OrgCrmSettings`. */\n crm?: OrgCrmSettings\n /** The workspace's email capture address (AGL-2657) — see `OrgCrmInbound`. */\n crmInbound?: OrgCrmInbound\n}\n\n/**\n * `orgs/{orgId}/members/{uid}` — THE authorization doc: rules resolve a\n * request with this single read. Owner/admin span every host; editor and\n * viewer see `hostAccess` (or `allHosts`).\n */\nexport interface AglynOrgMember extends AglynDocument {\n $id: UserUid\n role?: OrgRole\n /**\n * Custom role reference (AGL-243): id of an `orgs/{orgId}/roles` doc\n * whose permission map overrides the org role's defaults.\n */\n roleId?: string\n /** Per-member permission overrides (AGL-243); win over every layer. */\n permissions?: Record<string, boolean>\n /** Org-wide host access shortcut; otherwise `hostAccess` decides. */\n allHosts?: boolean\n hostAccess?: Record<HostUid, HostAccessRole>\n /**\n * Per-site permission overrides for a COLLABORATOR (AGL-2927), keyed by\n * the host the grant is for. Absent keys resolve by the host role's\n * default, so a document written before this field existed reads the same\n * as one written after it. Org-wide members are never consulted here:\n * their org role, custom role and `permissions` map decide.\n */\n hostPermissions?: Record<HostUid, Partial<Record<HostPermissionKey, boolean>>>\n /**\n * Denormalized reach as scope tokens (AGL-1038), so rules can intersect\n * it with a resource's `visibleTo` — they cannot derive it from\n * `hostAccess` because the rules language has no `.map()`. Written only\n * by `syncOrgAuthProjections`; never edit it by hand.\n */\n scopeTokens?: ScopeToken[]\n /**\n * Denormalized THREE-LAYER permission verdict, so rules can honor a custom\n * role they cannot resolve.\n *\n * `roleId` points at another document and `permissions` is only the top\n * layer, so a rule reading either alone answers a different question from\n * the server — and reproducing the precedence in CEL needs a second\n * cross-document get() plus correct handling of a dangling id. This is\n * `resolveOrgPermissions(member, customRole)`, already applied.\n *\n * ⚠️ ABSENT IS NOT EMPTY, and the rules must never treat it as either\n * \"everything allowed\" or \"nothing allowed\". Documents predating this\n * field, and any written by a path that bypasses\n * `syncOrgAuthProjections`, carry no map at all; the rules fall back to\n * the layers they CAN read — the role and the per-member overrides — which\n * is exactly the verdict they gave before this field existed. Written only\n * by `syncOrgAuthProjections` and by `createOrganization`'s inline stamp;\n * never edit it by hand.\n */\n resolvedPermissions?: Record<string, boolean>\n /** Denormalized for member lists without N user lookups. */\n displayName?: string\n email?: string\n invitedBy?: UserUid\n joinedAt?: ITimestamp\n /**\n * Denormalized org suspension flag (AGL-210) so host writes stay a\n * single rules read; maintained by the staff suspension API.\n */\n orgSuspended?: boolean\n}\n\n/** `orgs/{orgId}/invites/{inviteId}` — pending email invites. */\nexport interface AglynOrgInvite extends AglynDocument {\n $id: string\n email?: string\n role?: OrgRole\n allHosts?: boolean\n hostAccess?: Record<HostUid, HostAccessRole>\n invitedBy?: UserUid\n createdAt?: ITimestamp\n acceptedAt?: ITimestamp | null\n acceptedBy?: UserUid\n}\n\n/**\n * `orgSlugs/{slug}` — transactional uniqueness reservation; created and\n * deleted only by the org APIs (Admin SDK), publicly readable so the\n * console can resolve a workspace subdomain client-side.\n */\nexport interface OrgSlugReservation {\n orgId: OrgUid\n /**\n * Set when the previous holder renamed away — the tombstone that keeps old\n * workspace URLs redirecting until somebody else wants the name.\n */\n movedTo?: string\n /**\n * Epoch millis at which a PENDING reservation lapses (AGL-2585).\n *\n * Present only on a workspace created by an owner whose email was not yet\n * verified, which is every password signup: the address is held for them,\n * not granted to them, and the hold ends unless they confirm the address.\n * Absent means granted, and a grant never expires — so every workspace made\n * by a verified owner, and every one that predates this field, is outside\n * the rule entirely.\n *\n * Cleared by `reap-unverified-orgs` once the owner verifies. Public, like\n * the rest of this document, which is why nothing identifying the owner is\n * written beside it.\n */\n reservedUntil?: number\n}\n\n/**\n * `hostIndex/{hostId}` — server-written host → org resolver so the tenant\n * renderer and middleware find a host's org without scanning.\n */\nexport interface HostIndexEntry {\n orgId: OrgUid\n subdomain?: string\n}\n\n/**\n * `users/{uid}/orgs/{orgId}` — reverse index for \"my organizations\",\n * maintained transactionally with the member doc by the membership API.\n */\nexport interface UserOrgMembership extends AglynDocument {\n $id: OrgUid\n role?: OrgRole\n orgName?: string\n slug?: OrgSlug\n /**\n * Mirror of `isOrgWideMember(orgs/{orgId}/members/{uid})` (AGL-1032): does\n * this membership reach the whole org, or only a list of sites?\n *\n * `role` alone cannot answer it — `grantHostAccess` writes `role: 'viewer'`\n * here for a site collaborator, exactly what a genuine org-wide viewer\n * carries, so the console could not tell them apart without a second read\n * of the member doc on every org route. Denormalized because the console\n * navigation guard has to be synchronous: an async answer flashes the org\n * chrome before hiding it.\n *\n * ABSENT means org-wide. Rows predating the mirror carry no flag, and a\n * missing field must never lock a real member out of their own workspace\n * (the same legacy shape `isOrgWideMember` handles). Only an explicit\n * `false` scopes the console — see `isOrgWideMembership`.\n *\n * Navigation only. The Firestore rules are the access boundary (AGL-1026);\n * a stale mirror at worst shows a page whose reads then come back empty.\n */\n orgWide?: boolean\n}\n\n/**\n * `users/{uid}/hostMemberships/{hostId}` — reverse index of the sites a user\n * can reach, mirroring `hosts/{hostId}.memberRoles` (AGL-844). Denormalizes the\n * host name so the site switcher and subdomain→id routing query a user's own\n * sites, ordered and name-prefix-searched, without scanning the `hosts`\n * collection. Admin-SDK-maintained beside `memberRoles`; a best-effort\n * convenience index, never an authorization source (the rules still gate host\n * reads on `memberRoles`). NOT named `hosts` — that would collide with the\n * top-level `hosts` collection for collection-group rules/indexes.\n */\nexport interface UserHostMembership extends AglynDocument {\n $id: HostUid\n orgId?: OrgUid\n subdomain?: string\n displayName?: string\n nameLower?: string\n /**\n * Mirror of the host's `seo.favicon` (AGL-1071). The site switcher renders\n * from this projection rather than the host doc, so without the mirror it\n * showed the generic glyph for every site — the favicon feature worked in\n * the sites list and nowhere else. Absent when the site has none; writers\n * must DELETE it on clear rather than omit it, since the rows are written\n * with `{ merge: true }` and an omitted field would keep the old icon.\n */\n favicon?: string\n role?: HostAccessRole\n updatedAt?: ITimestamp\n}\n"],"names":["HOST_ACCESS_ROLE_KEYS","admin","editor","author","viewer","HOST_ACCESS_ROLES","Object","keys"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;CAMC,GA6DD;;;;;;;;;;;;CAYC,GACD,MAAMA,wBAAsD;IAC1DC,OAAO;IACPC,QAAQ;IACRC,QAAQ;IACRC,QAAQ;AACV;AACA,OAAO,MAAMC,oBAAoBC,OAAOC,IAAI,CAC1CP,uBACmB"}
|
|
@@ -472,6 +472,19 @@ export interface ConsoleNavItem {
|
|
|
472
472
|
* rather than leave a moved page living at two.
|
|
473
473
|
*/
|
|
474
474
|
legacyHrefs?: readonly string[];
|
|
475
|
+
/**
|
|
476
|
+
* Where this item's tab sits among the plugin tabs of its strip
|
|
477
|
+
* (AGL-3294): lower first, absent is 0, and a tie keeps registration
|
|
478
|
+
* order.
|
|
479
|
+
*
|
|
480
|
+
* Registration order is otherwise the order, and it is not a tab's to
|
|
481
|
+
* choose: it follows the plugin registry, which also orders the staff
|
|
482
|
+
* strip, the providers and every widget zone the plugin fills — so moving
|
|
483
|
+
* one tab by moving its plugin would move everything else it registers.
|
|
484
|
+
* This moves the tab and nothing else. The shell's own tabs are not in the
|
|
485
|
+
* comparison; the plugin tabs sit as one block between them.
|
|
486
|
+
*/
|
|
487
|
+
tabOrder?: number;
|
|
475
488
|
/**
|
|
476
489
|
* Dashboard header for the plugin page (title + icon), and the docs topic
|
|
477
490
|
* its help `?` explains.
|
|
@@ -1497,7 +1510,8 @@ export interface ConsoleOrgNavEntry {
|
|
|
1497
1510
|
}
|
|
1498
1511
|
/**
|
|
1499
1512
|
* Every registered {@link ConsoleExtension.orgNavItems} entry, in
|
|
1500
|
-
* registration order, for the
|
|
1513
|
+
* {@link ConsoleNavItem.tabOrder} and then registration order, for the
|
|
1514
|
+
* organization's tab strip (AGL-2974).
|
|
1501
1515
|
*
|
|
1502
1516
|
* Carries the extension whole rather than a flattened copy of two of its
|
|
1503
1517
|
* fields: a tab for a surface the reader cannot open is hidden, and deciding
|
|
@@ -437,13 +437,26 @@ export function unregisterConsoleExtension(pluginId) {
|
|
|
437
437
|
* shell appends these to its static tabs, so a plugin adds a menu item
|
|
438
438
|
* by registering here — no edit to the console's nav constants.
|
|
439
439
|
*/ export function listConsoleNavItems(enabledPluginIds) {
|
|
440
|
-
return listConsoleExtensions(enabledPluginIds).flatMap((extension)=>{
|
|
440
|
+
return inTabOrder(listConsoleExtensions(enabledPluginIds).flatMap((extension)=>{
|
|
441
441
|
var _extension_navItems;
|
|
442
442
|
return ((_extension_navItems = extension.navItems) != null ? _extension_navItems : []).map((navItem)=>_extends({}, navItem, {
|
|
443
443
|
pluginId: extension.pluginId,
|
|
444
444
|
featureFlag: extension.featureFlag
|
|
445
445
|
}));
|
|
446
|
-
});
|
|
446
|
+
}), (entry)=>entry.tabOrder);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* A strip's plugin entries by {@link ConsoleNavItem.tabOrder}, lower first,
|
|
450
|
+
* a tie in the order they arrived — which is registration order.
|
|
451
|
+
*/ function inTabOrder(entries, tabOrderOf) {
|
|
452
|
+
return entries.map((entry, index)=>{
|
|
453
|
+
var _tabOrderOf;
|
|
454
|
+
return {
|
|
455
|
+
entry,
|
|
456
|
+
index,
|
|
457
|
+
order: (_tabOrderOf = tabOrderOf(entry)) != null ? _tabOrderOf : 0
|
|
458
|
+
};
|
|
459
|
+
}).sort((a, b)=>a.order - b.order || a.index - b.index).map(({ entry })=>entry);
|
|
447
460
|
}
|
|
448
461
|
/**
|
|
449
462
|
* One nav item against one href: exact, a declared section beneath it, or —
|
|
@@ -526,20 +539,21 @@ function matchNavItemHref(navItem, itemHref, href) {
|
|
|
526
539
|
}
|
|
527
540
|
/**
|
|
528
541
|
* Every registered {@link ConsoleExtension.orgNavItems} entry, in
|
|
529
|
-
* registration order, for the
|
|
542
|
+
* {@link ConsoleNavItem.tabOrder} and then registration order, for the
|
|
543
|
+
* organization's tab strip (AGL-2974).
|
|
530
544
|
*
|
|
531
545
|
* Carries the extension whole rather than a flattened copy of two of its
|
|
532
546
|
* fields: a tab for a surface the reader cannot open is hidden, and deciding
|
|
533
547
|
* that takes the extension's `permission`, `featureFlag` and
|
|
534
548
|
* `upgradeNotice`, which is the same set the org route reads.
|
|
535
549
|
*/ export function listConsoleOrgNavItems(enabledPluginIds) {
|
|
536
|
-
return listConsoleExtensions(enabledPluginIds).flatMap((extension)=>{
|
|
550
|
+
return inTabOrder(listConsoleExtensions(enabledPluginIds).flatMap((extension)=>{
|
|
537
551
|
var _extension_orgNavItems;
|
|
538
552
|
return ((_extension_orgNavItems = extension.orgNavItems) != null ? _extension_orgNavItems : []).map((navItem)=>({
|
|
539
553
|
extension,
|
|
540
554
|
navItem
|
|
541
555
|
}));
|
|
542
|
-
});
|
|
556
|
+
}), (entry)=>entry.navItem.tabOrder);
|
|
543
557
|
}
|
|
544
558
|
/**
|
|
545
559
|
* {@link resolveConsolePluginPage} for the ORGANIZATION level (AGL-2974): an
|