@aglyn/tenant-feature-instance 1.0.0-beta.196 → 1.0.0-beta.198

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": "@aglyn/tenant-feature-instance",
3
- "version": "1.0.0-beta.196",
3
+ "version": "1.0.0-beta.198",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,11 +25,11 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.196",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.196",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.196",
31
- "@aglyn/shared-util-http": "1.0.0-beta.196",
32
- "@aglyn/shared-util-timestamp": "1.0.0-beta.196",
28
+ "@aglyn/aglyn": "1.0.0-beta.198",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.198",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.198",
31
+ "@aglyn/shared-util-http": "1.0.0-beta.198",
32
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.198",
33
33
  "@swc/helpers": "0.5.23",
34
34
  "lodash-es": "^4.18.1"
35
35
  },
@@ -23,6 +23,12 @@ export interface ScopeTokensState {
23
23
  * Defaults to org-wide while loading: the rules are the enforcement point,
24
24
  * and guessing "scoped" before the member doc arrives would flash an empty
25
25
  * library at people who can see everything.
26
+ *
27
+ * `loaded` answers for the member and org ASKED ABOUT NOW. A caller whose org
28
+ * id arrives after its first render — resolved from a site through the host
29
+ * index — would otherwise read the previous pass's answer, org-wide and
30
+ * loaded, while the member document it actually needs is still in flight,
31
+ * and send the one unfiltered query this hook exists to prevent.
26
32
  */
27
33
  export declare function useScopeTokens(orgId: string | undefined): ScopeTokensState;
28
34
  export default useScopeTokens;
@@ -17,7 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  * limitations under the License.
18
18
  */ import { isOrgWideMember, memberScopeTokens, ORG_SCOPE_TOKEN } from "@aglyn/aglyn";
19
19
  import { doc, getDoc } from "firebase/firestore";
20
- import { useEffect, useState } from "react";
20
+ import { useEffect, useMemo, useState } from "react";
21
21
  import { useFirestore, useUser } from "./firebase/firebase-services.js";
22
22
  /**
23
23
  * The signed-in user's scope tokens in an org (AGL-1044/1045).
@@ -37,15 +37,24 @@ import { useFirestore, useUser } from "./firebase/firebase-services.js";
37
37
  * Defaults to org-wide while loading: the rules are the enforcement point,
38
38
  * and guessing "scoped" before the member doc arrives would flash an empty
39
39
  * library at people who can see everything.
40
+ *
41
+ * `loaded` answers for the member and org ASKED ABOUT NOW. A caller whose org
42
+ * id arrives after its first render — resolved from a site through the host
43
+ * index — would otherwise read the previous pass's answer, org-wide and
44
+ * loaded, while the member document it actually needs is still in flight,
45
+ * and send the one unfiltered query this hook exists to prevent.
40
46
  */ export function useScopeTokens(orgId) {
41
47
  const { data: user } = useUser();
42
48
  const firestore = useFirestore();
49
+ const signedIn = user == null ? void 0 : user.uid;
50
+ /** Whose reach the state holds: `uid/orgId`, or `null` for no org. */ const asked = signedIn && orgId ? `${signedIn}/${orgId}` : null;
43
51
  const [state, setState] = useState({
44
52
  tokens: [
45
53
  ORG_SCOPE_TOKEN
46
54
  ],
47
55
  orgWide: true,
48
- loaded: false
56
+ loaded: false,
57
+ for: null
49
58
  });
50
59
  useEffect(()=>{
51
60
  const uid = user == null ? void 0 : user.uid;
@@ -55,10 +64,12 @@ import { useFirestore, useUser } from "./firebase/firebase-services.js";
55
64
  ORG_SCOPE_TOKEN
56
65
  ],
57
66
  orgWide: true,
58
- loaded: Boolean(uid)
67
+ loaded: Boolean(uid),
68
+ for: null
59
69
  });
60
70
  return;
61
71
  }
72
+ const key = `${uid}/${orgId}`;
62
73
  let cancelled = false;
63
74
  getDoc(doc(firestore, 'orgs', orgId, 'members', uid)).then((snapshot)=>{
64
75
  if (cancelled) return;
@@ -72,7 +83,8 @@ import { useFirestore, useUser } from "./firebase/firebase-services.js";
72
83
  setState({
73
84
  tokens: memberScopeTokens(member),
74
85
  orgWide: isOrgWideMember(member),
75
- loaded: true
86
+ loaded: true,
87
+ for: key
76
88
  });
77
89
  }).catch(()=>{
78
90
  if (!cancelled) {
@@ -81,7 +93,8 @@ import { useFirestore, useUser } from "./firebase/firebase-services.js";
81
93
  ORG_SCOPE_TOKEN
82
94
  ],
83
95
  orgWide: true,
84
- loaded: true
96
+ loaded: true,
97
+ for: key
85
98
  });
86
99
  }
87
100
  });
@@ -93,7 +106,16 @@ import { useFirestore, useUser } from "./firebase/firebase-services.js";
93
106
  orgId,
94
107
  user
95
108
  ]);
96
- return state;
109
+ const loaded = state.loaded && state.for === asked;
110
+ return useMemo(()=>({
111
+ tokens: state.tokens,
112
+ orgWide: state.orgWide,
113
+ loaded
114
+ }), [
115
+ state.tokens,
116
+ state.orgWide,
117
+ loaded
118
+ ]);
97
119
  }
98
120
  export default useScopeTokens;
99
121
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-scope-tokens.ts"],"sourcesContent":["'use client'\n\n/**\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\nimport {\n isOrgWideMember,\n memberScopeTokens,\n ORG_SCOPE_TOKEN,\n type AglynOrgMember,\n} from '@aglyn/aglyn'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useFirestore, useUser } from './firebase/firebase-services'\n\nexport interface ScopeTokensState {\n /** The caller's read set, for `array-contains-any` (AGL-1037). */\n tokens: string[]\n /** Org-wide members read everything and need no query filter. */\n orgWide: boolean\n loaded: boolean\n}\n\n/**\n * The signed-in user's scope tokens in an org (AGL-1044/1045).\n *\n * This is NOT a UI-gating convenience — it is required for correctness.\n * Under AGL-1041/1042 the rules evaluate per document on a LIST, and\n * Firestore rejects the WHOLE query if any candidate would fail. So a\n * scoped collaborator's unfiltered `collection(...)` read comes back\n * `permission-denied`, not filtered: without the matching\n * `array-contains-any` constraint the page does not show less, it shows\n * an error.\n *\n * Org-wide members deliberately get `orgWide: true` and no filter. Adding\n * one would cost them a composite index for no benefit, and would silently\n * hide any doc whose `visibleTo` the backfill has not reached.\n *\n * Defaults to org-wide while loading: the rules are the enforcement point,\n * and guessing \"scoped\" before the member doc arrives would flash an empty\n * library at people who can see everything.\n */\nexport function useScopeTokens(orgId: string | undefined): ScopeTokensState {\n const { data: user } = useUser()\n const firestore = useFirestore()\n const [state, setState] = useState<ScopeTokensState>({\n tokens: [ORG_SCOPE_TOKEN],\n orgWide: true,\n loaded: false,\n })\n\n useEffect(() => {\n const uid = (user as { uid?: string } | undefined)?.uid\n if (!uid || !orgId) {\n setState({ tokens: [ORG_SCOPE_TOKEN], orgWide: true, loaded: Boolean(uid) })\n return\n }\n let cancelled = false\n getDoc(doc(firestore, 'orgs', orgId, 'members', uid))\n .then((snapshot) => {\n if (cancelled) return\n const member = snapshot.exists()\n ? ({ $id: uid, ...snapshot.data() } as Partial<AglynOrgMember>)\n : undefined\n // `memberScopeTokens` owns the \"stored projection, else recompute\"\n // rule so the client and every Admin-SDK gate answer it the same\n // way — a member doc the AGL-1040 backfill has not reached must\n // still resolve to what `grantHostAccess` would have stamped.\n setState({\n tokens: memberScopeTokens(member),\n orgWide: isOrgWideMember(member),\n loaded: true,\n })\n })\n .catch(() => {\n if (!cancelled) {\n setState({ tokens: [ORG_SCOPE_TOKEN], orgWide: true, loaded: true })\n }\n })\n return () => {\n cancelled = true\n }\n }, [firestore, orgId, user])\n\n return state\n}\n\nexport default useScopeTokens\n"],"names":["isOrgWideMember","memberScopeTokens","ORG_SCOPE_TOKEN","doc","getDoc","useEffect","useState","useFirestore","useUser","useScopeTokens","orgId","data","user","firestore","state","setState","tokens","orgWide","loaded","uid","Boolean","cancelled","then","snapshot","member","exists","$id","undefined","catch"],"mappings":"AAAA;;AAEA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,eAAe,EACfC,iBAAiB,EACjBC,eAAe,QAEV,eAAc;AACrB,SAASC,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAChD,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,YAAY,EAAEC,OAAO,QAAQ,kCAA8B;AAUpE;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,eAAeC,KAAyB;IACtD,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGJ;IACvB,MAAMK,YAAYN;IAClB,MAAM,CAACO,OAAOC,SAAS,GAAGT,SAA2B;QACnDU,QAAQ;YAACd;SAAgB;QACzBe,SAAS;QACTC,QAAQ;IACV;IAEAb,UAAU;QACR,MAAMc,MAAOP,wBAAD,AAACA,KAAuCO,GAAG;QACvD,IAAI,CAACA,OAAO,CAACT,OAAO;YAClBK,SAAS;gBAAEC,QAAQ;oBAACd;iBAAgB;gBAAEe,SAAS;gBAAMC,QAAQE,QAAQD;YAAK;YAC1E;QACF;QACA,IAAIE,YAAY;QAChBjB,OAAOD,IAAIU,WAAW,QAAQH,OAAO,WAAWS,MAC7CG,IAAI,CAAC,CAACC;YACL,IAAIF,WAAW;YACf,MAAMG,SAASD,SAASE,MAAM,KACzB;gBAAEC,KAAKP;eAAQI,SAASZ,IAAI,MAC7BgB;YACJ,mEAAmE;YACnE,iEAAiE;YACjE,gEAAgE;YAChE,8DAA8D;YAC9DZ,SAAS;gBACPC,QAAQf,kBAAkBuB;gBAC1BP,SAASjB,gBAAgBwB;gBACzBN,QAAQ;YACV;QACF,GACCU,KAAK,CAAC;YACL,IAAI,CAACP,WAAW;gBACdN,SAAS;oBAAEC,QAAQ;wBAACd;qBAAgB;oBAAEe,SAAS;oBAAMC,QAAQ;gBAAK;YACpE;QACF;QACF,OAAO;YACLG,YAAY;QACd;IACF,GAAG;QAACR;QAAWH;QAAOE;KAAK;IAE3B,OAAOE;AACT;AAEA,eAAeL,eAAc"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-scope-tokens.ts"],"sourcesContent":["'use client'\n\n/**\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\nimport {\n isOrgWideMember,\n memberScopeTokens,\n ORG_SCOPE_TOKEN,\n type AglynOrgMember,\n} from '@aglyn/aglyn'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } from './firebase/firebase-services'\n\nexport interface ScopeTokensState {\n /** The caller's read set, for `array-contains-any` (AGL-1037). */\n tokens: string[]\n /** Org-wide members read everything and need no query filter. */\n orgWide: boolean\n loaded: boolean\n}\n\n/**\n * The signed-in user's scope tokens in an org (AGL-1044/1045).\n *\n * This is NOT a UI-gating convenience — it is required for correctness.\n * Under AGL-1041/1042 the rules evaluate per document on a LIST, and\n * Firestore rejects the WHOLE query if any candidate would fail. So a\n * scoped collaborator's unfiltered `collection(...)` read comes back\n * `permission-denied`, not filtered: without the matching\n * `array-contains-any` constraint the page does not show less, it shows\n * an error.\n *\n * Org-wide members deliberately get `orgWide: true` and no filter. Adding\n * one would cost them a composite index for no benefit, and would silently\n * hide any doc whose `visibleTo` the backfill has not reached.\n *\n * Defaults to org-wide while loading: the rules are the enforcement point,\n * and guessing \"scoped\" before the member doc arrives would flash an empty\n * library at people who can see everything.\n *\n * `loaded` answers for the member and org ASKED ABOUT NOW. A caller whose org\n * id arrives after its first render — resolved from a site through the host\n * index — would otherwise read the previous pass's answer, org-wide and\n * loaded, while the member document it actually needs is still in flight,\n * and send the one unfiltered query this hook exists to prevent.\n */\nexport function useScopeTokens(orgId: string | undefined): ScopeTokensState {\n const { data: user } = useUser()\n const firestore = useFirestore()\n const signedIn = (user as { uid?: string } | undefined)?.uid\n /** Whose reach the state holds: `uid/orgId`, or `null` for no org. */\n const asked = signedIn && orgId ? `${signedIn}/${orgId}` : null\n const [state, setState] = useState<ScopeTokensState & { for: string | null }>({\n tokens: [ORG_SCOPE_TOKEN],\n orgWide: true,\n loaded: false,\n for: null,\n })\n\n useEffect(() => {\n const uid = (user as { uid?: string } | undefined)?.uid\n if (!uid || !orgId) {\n setState({\n tokens: [ORG_SCOPE_TOKEN],\n orgWide: true,\n loaded: Boolean(uid),\n for: null,\n })\n return\n }\n const key = `${uid}/${orgId}`\n let cancelled = false\n getDoc(doc(firestore, 'orgs', orgId, 'members', uid))\n .then((snapshot) => {\n if (cancelled) return\n const member = snapshot.exists()\n ? ({ $id: uid, ...snapshot.data() } as Partial<AglynOrgMember>)\n : undefined\n // `memberScopeTokens` owns the \"stored projection, else recompute\"\n // rule so the client and every Admin-SDK gate answer it the same\n // way — a member doc the AGL-1040 backfill has not reached must\n // still resolve to what `grantHostAccess` would have stamped.\n setState({\n tokens: memberScopeTokens(member),\n orgWide: isOrgWideMember(member),\n loaded: true,\n for: key,\n })\n })\n .catch(() => {\n if (!cancelled) {\n setState({\n tokens: [ORG_SCOPE_TOKEN],\n orgWide: true,\n loaded: true,\n for: key,\n })\n }\n })\n return () => {\n cancelled = true\n }\n }, [firestore, orgId, user])\n\n const loaded = state.loaded && state.for === asked\n return useMemo(\n () => ({ tokens: state.tokens, orgWide: state.orgWide, loaded }),\n [state.tokens, state.orgWide, loaded],\n )\n}\n\nexport default useScopeTokens\n"],"names":["isOrgWideMember","memberScopeTokens","ORG_SCOPE_TOKEN","doc","getDoc","useEffect","useMemo","useState","useFirestore","useUser","useScopeTokens","orgId","data","user","firestore","signedIn","uid","asked","state","setState","tokens","orgWide","loaded","for","Boolean","key","cancelled","then","snapshot","member","exists","$id","undefined","catch"],"mappings":"AAAA;;AAEA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,eAAe,EACfC,iBAAiB,EACjBC,eAAe,QAEV,eAAc;AACrB,SAASC,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAChD,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SAASC,YAAY,EAAEC,OAAO,QAAQ,kCAA8B;AAUpE;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,SAASC,eAAeC,KAAyB;IACtD,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGJ;IACvB,MAAMK,YAAYN;IAClB,MAAMO,WAAYF,wBAAD,AAACA,KAAuCG,GAAG;IAC5D,oEAAoE,GACpE,MAAMC,QAAQF,YAAYJ,QAAQ,GAAGI,SAAS,CAAC,EAAEJ,OAAO,GAAG;IAC3D,MAAM,CAACO,OAAOC,SAAS,GAAGZ,SAAoD;QAC5Ea,QAAQ;YAAClB;SAAgB;QACzBmB,SAAS;QACTC,QAAQ;QACRC,KAAK;IACP;IAEAlB,UAAU;QACR,MAAMW,MAAOH,wBAAD,AAACA,KAAuCG,GAAG;QACvD,IAAI,CAACA,OAAO,CAACL,OAAO;YAClBQ,SAAS;gBACPC,QAAQ;oBAAClB;iBAAgB;gBACzBmB,SAAS;gBACTC,QAAQE,QAAQR;gBAChBO,KAAK;YACP;YACA;QACF;QACA,MAAME,MAAM,GAAGT,IAAI,CAAC,EAAEL,OAAO;QAC7B,IAAIe,YAAY;QAChBtB,OAAOD,IAAIW,WAAW,QAAQH,OAAO,WAAWK,MAC7CW,IAAI,CAAC,CAACC;YACL,IAAIF,WAAW;YACf,MAAMG,SAASD,SAASE,MAAM,KACzB;gBAAEC,KAAKf;eAAQY,SAAShB,IAAI,MAC7BoB;YACJ,mEAAmE;YACnE,iEAAiE;YACjE,gEAAgE;YAChE,8DAA8D;YAC9Db,SAAS;gBACPC,QAAQnB,kBAAkB4B;gBAC1BR,SAASrB,gBAAgB6B;gBACzBP,QAAQ;gBACRC,KAAKE;YACP;QACF,GACCQ,KAAK,CAAC;YACL,IAAI,CAACP,WAAW;gBACdP,SAAS;oBACPC,QAAQ;wBAAClB;qBAAgB;oBACzBmB,SAAS;oBACTC,QAAQ;oBACRC,KAAKE;gBACP;YACF;QACF;QACF,OAAO;YACLC,YAAY;QACd;IACF,GAAG;QAACZ;QAAWH;QAAOE;KAAK;IAE3B,MAAMS,SAASJ,MAAMI,MAAM,IAAIJ,MAAMK,GAAG,KAAKN;IAC7C,OAAOX,QACL,IAAO,CAAA;YAAEc,QAAQF,MAAME,MAAM;YAAEC,SAASH,MAAMG,OAAO;YAAEC;QAAO,CAAA,GAC9D;QAACJ,MAAME,MAAM;QAAEF,MAAMG,OAAO;QAAEC;KAAO;AAEzC;AAEA,eAAeZ,eAAc"}