@aglyn/tenant-feature-instance 1.0.0-beta.143

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.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +3 -0
  3. package/jest.config.d.ts +36 -0
  4. package/package.json +45 -0
  5. package/src/index.d.ts +17 -0
  6. package/src/index.js +18 -0
  7. package/src/index.js.map +1 -0
  8. package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
  9. package/src/lib/components/duplicate-resource-dialog.js +217 -0
  10. package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
  11. package/src/lib/constants/firebase-config.d.ts +68 -0
  12. package/src/lib/constants/firebase-config.js +155 -0
  13. package/src/lib/constants/firebase-config.js.map +1 -0
  14. package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
  15. package/src/lib/constants/firebase-emulator-hosts.js +81 -0
  16. package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
  17. package/src/lib/constants/mock-data.d.ts +24 -0
  18. package/src/lib/constants/mock-data.js +49 -0
  19. package/src/lib/constants/mock-data.js.map +1 -0
  20. package/src/lib/constants/site-paths.d.ts +47 -0
  21. package/src/lib/constants/site-paths.js +66 -0
  22. package/src/lib/constants/site-paths.js.map +1 -0
  23. package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
  24. package/src/lib/hooks/firebase/auth-persistence.js +117 -0
  25. package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
  26. package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
  27. package/src/lib/hooks/firebase/firebase-services.js +522 -0
  28. package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
  29. package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
  30. package/src/lib/hooks/firebase/firestore-cache.js +152 -0
  31. package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
  32. package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
  33. package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
  34. package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
  35. package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
  36. package/src/lib/hooks/firestore-denial-reporter.js +293 -0
  37. package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
  38. package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
  39. package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
  40. package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
  41. package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
  42. package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
  43. package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
  44. package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
  45. package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
  46. package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
  47. package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
  48. package/src/lib/hooks/helpers/listen-options.js +92 -0
  49. package/src/lib/hooks/helpers/listen-options.js.map +1 -0
  50. package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
  51. package/src/lib/hooks/helpers/use-doc.js +213 -0
  52. package/src/lib/hooks/helpers/use-doc.js.map +1 -0
  53. package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
  54. package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
  55. package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
  56. package/src/lib/hooks/host-collection-queries.d.ts +125 -0
  57. package/src/lib/hooks/host-collection-queries.js +126 -0
  58. package/src/lib/hooks/host-collection-queries.js.map +1 -0
  59. package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
  60. package/src/lib/hooks/list-filter-constraints.js +288 -0
  61. package/src/lib/hooks/list-filter-constraints.js.map +1 -0
  62. package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
  63. package/src/lib/hooks/sorted-collection-window.js +150 -0
  64. package/src/lib/hooks/sorted-collection-window.js.map +1 -0
  65. package/src/lib/hooks/use-component-version.d.ts +43 -0
  66. package/src/lib/hooks/use-component-version.js +73 -0
  67. package/src/lib/hooks/use-component-version.js.map +1 -0
  68. package/src/lib/hooks/use-component.d.ts +39 -0
  69. package/src/lib/hooks/use-component.js +55 -0
  70. package/src/lib/hooks/use-component.js.map +1 -0
  71. package/src/lib/hooks/use-console-host-route.d.ts +24 -0
  72. package/src/lib/hooks/use-console-host-route.js +77 -0
  73. package/src/lib/hooks/use-console-host-route.js.map +1 -0
  74. package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
  75. package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
  76. package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
  77. package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
  78. package/src/lib/hooks/use-firestore-collection.js +307 -0
  79. package/src/lib/hooks/use-firestore-collection.js.map +1 -0
  80. package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
  81. package/src/lib/hooks/use-firestore-doc.js +192 -0
  82. package/src/lib/hooks/use-firestore-doc.js.map +1 -0
  83. package/src/lib/hooks/use-form-version.d.ts +44 -0
  84. package/src/lib/hooks/use-form-version.js +73 -0
  85. package/src/lib/hooks/use-form-version.js.map +1 -0
  86. package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
  87. package/src/lib/hooks/use-host-activity-logger.js +96 -0
  88. package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
  89. package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
  90. package/src/lib/hooks/use-host-campaigns.js +93 -0
  91. package/src/lib/hooks/use-host-campaigns.js.map +1 -0
  92. package/src/lib/hooks/use-host-org-id.d.ts +96 -0
  93. package/src/lib/hooks/use-host-org-id.js +154 -0
  94. package/src/lib/hooks/use-host-org-id.js.map +1 -0
  95. package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
  96. package/src/lib/hooks/use-host-resource-api.js +60 -0
  97. package/src/lib/hooks/use-host-resource-api.js.map +1 -0
  98. package/src/lib/hooks/use-host-template.d.ts +44 -0
  99. package/src/lib/hooks/use-host-template.js +82 -0
  100. package/src/lib/hooks/use-host-template.js.map +1 -0
  101. package/src/lib/hooks/use-host-version-api.d.ts +59 -0
  102. package/src/lib/hooks/use-host-version-api.js +69 -0
  103. package/src/lib/hooks/use-host-version-api.js.map +1 -0
  104. package/src/lib/hooks/use-host.d.ts +101 -0
  105. package/src/lib/hooks/use-host.js +45 -0
  106. package/src/lib/hooks/use-host.js.map +1 -0
  107. package/src/lib/hooks/use-layout-version.d.ts +33 -0
  108. package/src/lib/hooks/use-layout-version.js +64 -0
  109. package/src/lib/hooks/use-layout-version.js.map +1 -0
  110. package/src/lib/hooks/use-layout.d.ts +41 -0
  111. package/src/lib/hooks/use-layout.js +49 -0
  112. package/src/lib/hooks/use-layout.js.map +1 -0
  113. package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
  114. package/src/lib/hooks/use-live-artifact-count.js +102 -0
  115. package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
  116. package/src/lib/hooks/use-org-member-options.d.ts +57 -0
  117. package/src/lib/hooks/use-org-member-options.js +112 -0
  118. package/src/lib/hooks/use-org-member-options.js.map +1 -0
  119. package/src/lib/hooks/use-org-plan.d.ts +46 -0
  120. package/src/lib/hooks/use-org-plan.js +86 -0
  121. package/src/lib/hooks/use-org-plan.js.map +1 -0
  122. package/src/lib/hooks/use-paged-collection.d.ts +95 -0
  123. package/src/lib/hooks/use-paged-collection.js +130 -0
  124. package/src/lib/hooks/use-paged-collection.js.map +1 -0
  125. package/src/lib/hooks/use-plugin-config.d.ts +38 -0
  126. package/src/lib/hooks/use-plugin-config.js +120 -0
  127. package/src/lib/hooks/use-plugin-config.js.map +1 -0
  128. package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
  129. package/src/lib/hooks/use-scope-tokens.js +100 -0
  130. package/src/lib/hooks/use-scope-tokens.js.map +1 -0
  131. package/src/lib/hooks/use-screen-version.d.ts +33 -0
  132. package/src/lib/hooks/use-screen-version.js +65 -0
  133. package/src/lib/hooks/use-screen-version.js.map +1 -0
  134. package/src/lib/hooks/use-screen.d.ts +68 -0
  135. package/src/lib/hooks/use-screen.js +50 -0
  136. package/src/lib/hooks/use-screen.js.map +1 -0
  137. package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
  138. package/src/lib/hooks/use-sending-identity-api.js +95 -0
  139. package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
  140. package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
  141. package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
  142. package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
  143. package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
  144. package/src/lib/hooks/use-switcher-collection.js +275 -0
  145. package/src/lib/hooks/use-switcher-collection.js.map +1 -0
  146. package/src/lib/hooks/use-user-name.d.ts +51 -0
  147. package/src/lib/hooks/use-user-name.js +90 -0
  148. package/src/lib/hooks/use-user-name.js.map +1 -0
  149. package/src/lib/hooks/use-user-photo.d.ts +50 -0
  150. package/src/lib/hooks/use-user-photo.js +87 -0
  151. package/src/lib/hooks/use-user-photo.js.map +1 -0
  152. package/src/lib/tenant.d.ts +61 -0
  153. package/src/lib/tenant.js +62 -0
  154. package/src/lib/tenant.js.map +1 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
18
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
19
+ import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
20
+ import { useFirestoreCollection } from "./use-firestore-collection.js";
21
+ /**
22
+ * A paged window over a live collection, instead of a big read sliced small.
23
+ *
24
+ * ## The shape this replaces
25
+ *
26
+ * A card would listen with `limit(300)` and render `rows.slice(0, 50)`. That
27
+ * is wrong twice over. Two hundred and fifty documents are read, billed and
28
+ * discarded on every mount — and the rows past the slice are not merely
29
+ * wasted, they are UNREACHABLE, because nothing renders them and no control
30
+ * asks for more. A merchant with sixty gift cards sees fifty and no
31
+ * indication that ten more exist; past three hundred, a card issued this
32
+ * morning cannot be reached at all.
33
+ *
34
+ * Here the window IS the query, and it is expressed in PAGES so the same
35
+ * `ListPagination` footer serves this and a cursor feed. The listener is
36
+ * widened to cover every page up to the one being read, so paging BACK costs
37
+ * nothing — the rows are already in the snapshot — and paging forward costs
38
+ * one page more.
39
+ *
40
+ * ## Why the window covers page 0..n rather than page n alone
41
+ *
42
+ * This is a live `onSnapshot`, not a fetch. Firestore cursors need a document
43
+ * snapshot to resume from, which a listener that has never read page n - 1
44
+ * does not have. Widening the limit is what makes forward and backward
45
+ * symmetrical without a second read path, and the cost is bounded by how deep
46
+ * the reader actually goes — with the shared default of ten, page three is
47
+ * thirty-one documents, and most readers never leave page one.
48
+ *
49
+ * ## Why over-fetch by one
50
+ *
51
+ * `hasMore` has to be a fact. Comparing `length === limit` is wrong exactly
52
+ * when the count is an even multiple of the page size — the case that offers
53
+ * a "next page" leading nowhere, or hides one that leads somewhere. The probe
54
+ * row answers it outright and is never rendered.
55
+ *
56
+ * ## Why the window resets on `deps`
57
+ *
58
+ * `deps` identify the SUBJECT being listened to. Without the reset, a card
59
+ * paged three deep on one site opens the next site three pages in — a read
60
+ * the reader never asked for, charged to whoever they switched to.
61
+ *
62
+ * `buildQuery` receives the limit to apply, so the caller keeps ownership of
63
+ * ordering and predicates:
64
+ *
65
+ * ```ts
66
+ * usePagedCollection<Card>(
67
+ * (pageLimit) =>
68
+ * hostId
69
+ * ? query(collection(firestore, 'hosts', hostId, 'giftCards'),
70
+ * orderBy('createdAt', 'desc'), limit(pageLimit))
71
+ * : null,
72
+ * [firestore, hostId],
73
+ * { idField: '$id' },
74
+ * )
75
+ * ```
76
+ */ export function usePagedCollection(buildQuery, deps, options = {}) {
77
+ var _ref;
78
+ var _result_data;
79
+ const { pageSize: initialPageSize = TABLE_PAGE_SIZE_DEFAULT } = options, collectionOptions = _object_without_properties_loose(options, [
80
+ "pageSize"
81
+ ]);
82
+ const [pageSize, setPageSizeState] = useState(initialPageSize);
83
+ const [page, setPage] = useState(0);
84
+ const buildQueryRef = useRef(buildQuery);
85
+ buildQueryRef.current = buildQuery;
86
+ /*
87
+ * A new subject starts at page one. Spreading `deps` is the same contract
88
+ * `useFirestoreCollection` documents — pass the primitive ids, not the
89
+ * Query.
90
+ */ useEffect(()=>{
91
+ setPage(0);
92
+ // eslint-disable-next-line react-hooks/exhaustive-deps
93
+ }, [
94
+ ...deps
95
+ ]);
96
+ // Everything up to and including the page being read, plus the probe row.
97
+ const windowSize = pageSize * (page + 1);
98
+ const result = useFirestoreCollection(()=>buildQueryRef.current(windowSize + 1), // eslint-disable-next-line react-hooks/exhaustive-deps
99
+ [
100
+ ...deps,
101
+ windowSize
102
+ ], collectionOptions);
103
+ const hasMore = ((_ref = (_result_data = result.data) == null ? void 0 : _result_data.length) != null ? _ref : 0) > windowSize;
104
+ const rows = useMemo(()=>{
105
+ var _result_data;
106
+ return ((_result_data = result.data) != null ? _result_data : []).slice(page * pageSize, windowSize);
107
+ }, [
108
+ result.data,
109
+ page,
110
+ pageSize,
111
+ windowSize
112
+ ]);
113
+ const setPageSize = useCallback((next)=>{
114
+ setPageSizeState(next);
115
+ // Page four of a ten-row list does not exist once the reader asks for
116
+ // fifty at a time, and an out-of-range page renders as an empty list
117
+ // with no explanation — which reads as the data having gone.
118
+ setPage(0);
119
+ }, []);
120
+ return _extends({}, result, {
121
+ rows,
122
+ hasMore,
123
+ page,
124
+ setPage,
125
+ pageSize,
126
+ setPageSize
127
+ });
128
+ }
129
+
130
+ //# sourceMappingURL=use-paged-collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-paged-collection.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\nimport type { DocumentData, Query } from 'firebase/firestore'\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type DependencyList,\n} from 'react'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n useFirestoreCollection,\n type UseFirestoreCollectionOptions,\n type UseFirestoreCollectionResult,\n} from './use-firestore-collection'\n\nexport interface UsePagedCollectionOptions\n extends UseFirestoreCollectionOptions {\n /**\n * Rows per page. Defaults to the console-wide smallest option — a list\n * that picks its own number is how five different ones appear, and on a\n * query bounded by its page size the smallest page is the smallest bill.\n */\n pageSize?: number\n}\n\nexport interface UsePagedCollectionResult<T>\n extends UseFirestoreCollectionResult<T> {\n /** The current page's rows. `data` holds the probe row; this never does. */\n rows: T[]\n /** A further page exists. A FACT from the probe row, not a guess. */\n hasMore: boolean\n /** Zero-based, to match `ListPagination` and MUI. */\n page: number\n setPage: (page: number) => void\n pageSize: number\n setPageSize: (pageSize: number) => void\n}\n\n/**\n * A paged window over a live collection, instead of a big read sliced small.\n *\n * ## The shape this replaces\n *\n * A card would listen with `limit(300)` and render `rows.slice(0, 50)`. That\n * is wrong twice over. Two hundred and fifty documents are read, billed and\n * discarded on every mount — and the rows past the slice are not merely\n * wasted, they are UNREACHABLE, because nothing renders them and no control\n * asks for more. A merchant with sixty gift cards sees fifty and no\n * indication that ten more exist; past three hundred, a card issued this\n * morning cannot be reached at all.\n *\n * Here the window IS the query, and it is expressed in PAGES so the same\n * `ListPagination` footer serves this and a cursor feed. The listener is\n * widened to cover every page up to the one being read, so paging BACK costs\n * nothing — the rows are already in the snapshot — and paging forward costs\n * one page more.\n *\n * ## Why the window covers page 0..n rather than page n alone\n *\n * This is a live `onSnapshot`, not a fetch. Firestore cursors need a document\n * snapshot to resume from, which a listener that has never read page n - 1\n * does not have. Widening the limit is what makes forward and backward\n * symmetrical without a second read path, and the cost is bounded by how deep\n * the reader actually goes — with the shared default of ten, page three is\n * thirty-one documents, and most readers never leave page one.\n *\n * ## Why over-fetch by one\n *\n * `hasMore` has to be a fact. Comparing `length === limit` is wrong exactly\n * when the count is an even multiple of the page size — the case that offers\n * a \"next page\" leading nowhere, or hides one that leads somewhere. The probe\n * row answers it outright and is never rendered.\n *\n * ## Why the window resets on `deps`\n *\n * `deps` identify the SUBJECT being listened to. Without the reset, a card\n * paged three deep on one site opens the next site three pages in — a read\n * the reader never asked for, charged to whoever they switched to.\n *\n * `buildQuery` receives the limit to apply, so the caller keeps ownership of\n * ordering and predicates:\n *\n * ```ts\n * usePagedCollection<Card>(\n * (pageLimit) =>\n * hostId\n * ? query(collection(firestore, 'hosts', hostId, 'giftCards'),\n * orderBy('createdAt', 'desc'), limit(pageLimit))\n * : null,\n * [firestore, hostId],\n * { idField: '$id' },\n * )\n * ```\n */\nexport function usePagedCollection<T = DocumentData>(\n buildQuery: (pageLimit: number) => Query<DocumentData> | null | undefined,\n deps: DependencyList,\n options: UsePagedCollectionOptions = {},\n): UsePagedCollectionResult<T> {\n const { pageSize: initialPageSize = TABLE_PAGE_SIZE_DEFAULT, ...collectionOptions } =\n options\n const [pageSize, setPageSizeState] = useState(initialPageSize)\n const [page, setPage] = useState(0)\n const buildQueryRef = useRef(buildQuery)\n buildQueryRef.current = buildQuery\n\n /*\n * A new subject starts at page one. Spreading `deps` is the same contract\n * `useFirestoreCollection` documents — pass the primitive ids, not the\n * Query.\n */\n useEffect(() => {\n setPage(0)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [...deps])\n\n // Everything up to and including the page being read, plus the probe row.\n const windowSize = pageSize * (page + 1)\n const result = useFirestoreCollection<T>(\n () => buildQueryRef.current(windowSize + 1),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [...deps, windowSize],\n collectionOptions,\n )\n\n const hasMore = (result.data?.length ?? 0) > windowSize\n const rows = useMemo(\n () => (result.data ?? []).slice(page * pageSize, windowSize),\n [result.data, page, pageSize, windowSize],\n )\n const setPageSize = useCallback((next: number) => {\n setPageSizeState(next)\n // Page four of a ten-row list does not exist once the reader asks for\n // fifty at a time, and an out-of-range page renders as an empty list\n // with no explanation — which reads as the data having gone.\n setPage(0)\n }, [])\n\n return { ...result, rows, hasMore, page, setPage, pageSize, setPageSize }\n}\n"],"names":["useCallback","useEffect","useMemo","useRef","useState","TABLE_PAGE_SIZE_DEFAULT","useFirestoreCollection","usePagedCollection","buildQuery","deps","options","result","pageSize","initialPageSize","collectionOptions","setPageSizeState","page","setPage","buildQueryRef","current","windowSize","hasMore","data","length","rows","slice","setPageSize","next"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;;AAGD,SACEA,WAAW,EACXC,SAAS,EACTC,OAAO,EACPC,MAAM,EACNC,QAAQ,QAEH,QAAO;AACd,SAASC,uBAAuB,QAAQ,8CAA6C;AACrF,SACEC,sBAAsB,QAGjB,gCAA4B;AAyBnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDC,GACD,OAAO,SAASC,mBACdC,UAAyE,EACzEC,IAAoB,EACpBC,UAAqC,CAAC,CAAC;;QA4BtBC;IA1BjB,MAAM,EAAEC,UAAUC,kBAAkBR,uBAAuB,EAAwB,GACjFK,SAD8DI,qDAC9DJ;;;IACF,MAAM,CAACE,UAAUG,iBAAiB,GAAGX,SAASS;IAC9C,MAAM,CAACG,MAAMC,QAAQ,GAAGb,SAAS;IACjC,MAAMc,gBAAgBf,OAAOK;IAC7BU,cAAcC,OAAO,GAAGX;IAExB;;;;GAIC,GACDP,UAAU;QACRgB,QAAQ;IACR,uDAAuD;IACzD,GAAG;WAAIR;KAAK;IAEZ,0EAA0E;IAC1E,MAAMW,aAAaR,WAAYI,CAAAA,OAAO,CAAA;IACtC,MAAML,SAASL,uBACb,IAAMY,cAAcC,OAAO,CAACC,aAAa,IACzC,uDAAuD;IACvD;WAAIX;QAAMW;KAAW,EACrBN;IAGF,MAAMO,UAAU,UAACV,eAAAA,OAAOW,IAAI,qBAAXX,aAAaY,MAAM,mBAAI,KAAKH;IAC7C,MAAMI,OAAOtB,QACX;YAAOS;eAAD,EAACA,eAAAA,OAAOW,IAAI,YAAXX,eAAe,EAAE,EAAEc,KAAK,CAACT,OAAOJ,UAAUQ;OACjD;QAACT,OAAOW,IAAI;QAAEN;QAAMJ;QAAUQ;KAAW;IAE3C,MAAMM,cAAc1B,YAAY,CAAC2B;QAC/BZ,iBAAiBY;QACjB,sEAAsE;QACtE,qEAAqE;QACrE,6DAA6D;QAC7DV,QAAQ;IACV,GAAG,EAAE;IAEL,OAAO,aAAKN;QAAQa;QAAMH;QAASL;QAAMC;QAASL;QAAUc;;AAC9D"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Live per-plugin config for a workspace (AGL-428): the org's
3
+ * `pluginSettings/{pluginId}` overrides merged over the plugin's declared
4
+ * schema defaults (type-coerced). Plugins call this from console pages/
5
+ * widgets with the orgId the shell resolves; `ready` turns true once the
6
+ * doc read settles either way.
7
+ */
8
+ export declare function usePluginConfig(orgId: string | null | undefined, pluginId: string): {
9
+ config: Record<string, unknown>;
10
+ ready: boolean;
11
+ };
12
+ /**
13
+ * The same config, resolved for ONE SITE (AGL-428, AGL-1014): the
14
+ * workspace's stored overrides, then only the keys this site answers for
15
+ * itself, over the plugin's declared schema defaults. `overrides` names those keys, so a
16
+ * surface can say which answers are the site's own rather than inferring it
17
+ * by comparing values — two levels that happen to agree are still two levels.
18
+ *
19
+ * BOTH refs are held at null until their id is known, never addressed as
20
+ * `orgs/-pending-` or `hosts/-pending-` (AGL-1440). `pluginSettings` is
21
+ * member-gated at both scopes, so a sentinel id is a guaranteed permission
22
+ * denial on every mount rather than a read that might succeed — the
23
+ * anti-pattern `use-org-plan.ts` documents removing (AGL-1064). A null ref
24
+ * issues nothing and holds `status` at `'loading'`, which is already this
25
+ * hook's contract while the answer is unknown.
26
+ *
27
+ * `ready` needs BOTH reads, and needs both to have SUCCEEDED (AGL-1066).
28
+ * Answering ready off the org read alone would hand a caller the workspace's
29
+ * value as though it were this site's, which is the exact failure the site
30
+ * layer exists to prevent — and answering it off a refused read would hand
31
+ * them schema defaults as though they were stored settings.
32
+ */
33
+ export declare function useSitePluginConfig(orgId: string | null | undefined, hostId: string | null | undefined, pluginId: string): {
34
+ config: Record<string, unknown>;
35
+ overrides: string[];
36
+ ready: boolean;
37
+ };
38
+ export default usePluginConfig;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { getPluginConfigSchema, mergePluginConfig, pluginConfigOverrides, resolvePluginConfig } from "@aglyn/aglyn";
19
+ import { doc } from "firebase/firestore";
20
+ import { useMemo } from "react";
21
+ import { useFirestore } from "./firebase/firebase-services.js";
22
+ import { useFirestoreDoc } from "./use-firestore-doc.js";
23
+ /**
24
+ * Live per-plugin config for a workspace (AGL-428): the org's
25
+ * `pluginSettings/{pluginId}` overrides merged over the plugin's declared
26
+ * schema defaults (type-coerced). Plugins call this from console pages/
27
+ * widgets with the orgId the shell resolves; `ready` turns true once the
28
+ * doc read settles either way.
29
+ */ export function usePluginConfig(orgId, pluginId) {
30
+ const firestore = useFirestore();
31
+ // Held at null while the org is unknown, never addressed as
32
+ // `orgs/-pending-` (AGL-1440): `pluginSettings` is member-gated, so the
33
+ // sentinel was a guaranteed-denied read on every mount of every plugin
34
+ // surface — the exact anti-pattern `use-org-plan.ts` documents removing
35
+ // (AGL-1064). A null ref issues nothing, `status` stays `'loading'`, and
36
+ // `ready` below stays false, which is already this hook's contract while
37
+ // the answer is unknown.
38
+ const { data, status } = useFirestoreDoc(()=>orgId ? doc(firestore, 'orgs', orgId, 'pluginSettings', pluginId) : null, [
39
+ firestore,
40
+ orgId,
41
+ pluginId
42
+ ]);
43
+ const config = useMemo(()=>{
44
+ const schema = getPluginConfigSchema(pluginId);
45
+ if (!schema) return data != null ? data : {};
46
+ return mergePluginConfig(schema, data != null ? data : null);
47
+ }, [
48
+ pluginId,
49
+ data
50
+ ]);
51
+ // `'success'` rather than `!== 'loading'` (AGL-1066): a refused listen can
52
+ // now reach `'error'`, and answering "ready" with an empty config would let
53
+ // a caller act on schema defaults as though they were the stored settings.
54
+ return {
55
+ config,
56
+ ready: status === 'success'
57
+ };
58
+ }
59
+ /**
60
+ * The same config, resolved for ONE SITE (AGL-428, AGL-1014): the
61
+ * workspace's stored overrides, then only the keys this site answers for
62
+ * itself, over the plugin's declared schema defaults. `overrides` names those keys, so a
63
+ * surface can say which answers are the site's own rather than inferring it
64
+ * by comparing values — two levels that happen to agree are still two levels.
65
+ *
66
+ * BOTH refs are held at null until their id is known, never addressed as
67
+ * `orgs/-pending-` or `hosts/-pending-` (AGL-1440). `pluginSettings` is
68
+ * member-gated at both scopes, so a sentinel id is a guaranteed permission
69
+ * denial on every mount rather than a read that might succeed — the
70
+ * anti-pattern `use-org-plan.ts` documents removing (AGL-1064). A null ref
71
+ * issues nothing and holds `status` at `'loading'`, which is already this
72
+ * hook's contract while the answer is unknown.
73
+ *
74
+ * `ready` needs BOTH reads, and needs both to have SUCCEEDED (AGL-1066).
75
+ * Answering ready off the org read alone would hand a caller the workspace's
76
+ * value as though it were this site's, which is the exact failure the site
77
+ * layer exists to prevent — and answering it off a refused read would hand
78
+ * them schema defaults as though they were stored settings.
79
+ */ export function useSitePluginConfig(orgId, hostId, pluginId) {
80
+ const firestore = useFirestore();
81
+ const { data: org, status: orgStatus } = useFirestoreDoc(()=>orgId ? doc(firestore, 'orgs', orgId, 'pluginSettings', pluginId) : null, [
82
+ firestore,
83
+ orgId,
84
+ pluginId
85
+ ]);
86
+ const { data: host, status: hostStatus } = useFirestoreDoc(()=>hostId ? doc(firestore, 'hosts', hostId, 'pluginSettings', pluginId) : null, [
87
+ firestore,
88
+ hostId,
89
+ pluginId
90
+ ]);
91
+ const resolved = useMemo(()=>{
92
+ const schema = getPluginConfigSchema(pluginId);
93
+ // Without a registered schema there is no field list to layer by, so the
94
+ // site's raw document simply narrows the workspace's — the same degrade
95
+ // `usePluginConfig` makes, one level deeper.
96
+ if (!schema) {
97
+ return {
98
+ config: _extends({}, org != null ? org : {}, host != null ? host : {}),
99
+ overrides: Object.keys(host != null ? host : {})
100
+ };
101
+ }
102
+ return {
103
+ config: resolvePluginConfig(schema, {
104
+ org: org != null ? org : null,
105
+ host: host != null ? host : null
106
+ }),
107
+ overrides: pluginConfigOverrides(schema, host != null ? host : null)
108
+ };
109
+ }, [
110
+ pluginId,
111
+ org,
112
+ host
113
+ ]);
114
+ return _extends({}, resolved, {
115
+ ready: orgStatus === 'success' && hostStatus === 'success'
116
+ });
117
+ }
118
+ export default usePluginConfig;
119
+
120
+ //# sourceMappingURL=use-plugin-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-plugin-config.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'use client'\n\nimport {\n getPluginConfigSchema,\n mergePluginConfig,\n pluginConfigOverrides,\n resolvePluginConfig,\n} from '@aglyn/aglyn'\nimport { doc } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from './firebase/firebase-services'\nimport { useFirestoreDoc } from './use-firestore-doc'\n\n/**\n * Live per-plugin config for a workspace (AGL-428): the org's\n * `pluginSettings/{pluginId}` overrides merged over the plugin's declared\n * schema defaults (type-coerced). Plugins call this from console pages/\n * widgets with the orgId the shell resolves; `ready` turns true once the\n * doc read settles either way.\n */\nexport function usePluginConfig(\n orgId: string | null | undefined,\n pluginId: string,\n): { config: Record<string, unknown>; ready: boolean } {\n const firestore = useFirestore()\n // Held at null while the org is unknown, never addressed as\n // `orgs/-pending-` (AGL-1440): `pluginSettings` is member-gated, so the\n // sentinel was a guaranteed-denied read on every mount of every plugin\n // surface — the exact anti-pattern `use-org-plan.ts` documents removing\n // (AGL-1064). A null ref issues nothing, `status` stays `'loading'`, and\n // `ready` below stays false, which is already this hook's contract while\n // the answer is unknown.\n const { data, status } = useFirestoreDoc<Record<string, unknown>>(\n () =>\n orgId\n ? doc(firestore, 'orgs', orgId, 'pluginSettings', pluginId)\n : null,\n [firestore, orgId, pluginId],\n )\n const config = useMemo(() => {\n const schema = getPluginConfigSchema(pluginId)\n if (!schema) return (data as Record<string, unknown>) ?? {}\n return mergePluginConfig(schema, data ?? null)\n }, [pluginId, data])\n // `'success'` rather than `!== 'loading'` (AGL-1066): a refused listen can\n // now reach `'error'`, and answering \"ready\" with an empty config would let\n // a caller act on schema defaults as though they were the stored settings.\n return { config, ready: status === 'success' }\n}\n\n/**\n * The same config, resolved for ONE SITE (AGL-428, AGL-1014): the\n * workspace's stored overrides, then only the keys this site answers for\n * itself, over the plugin's declared schema defaults. `overrides` names those keys, so a\n * surface can say which answers are the site's own rather than inferring it\n * by comparing values — two levels that happen to agree are still two levels.\n *\n * BOTH refs are held at null until their id is known, never addressed as\n * `orgs/-pending-` or `hosts/-pending-` (AGL-1440). `pluginSettings` is\n * member-gated at both scopes, so a sentinel id is a guaranteed permission\n * denial on every mount rather than a read that might succeed — the\n * anti-pattern `use-org-plan.ts` documents removing (AGL-1064). A null ref\n * issues nothing and holds `status` at `'loading'`, which is already this\n * hook's contract while the answer is unknown.\n *\n * `ready` needs BOTH reads, and needs both to have SUCCEEDED (AGL-1066).\n * Answering ready off the org read alone would hand a caller the workspace's\n * value as though it were this site's, which is the exact failure the site\n * layer exists to prevent — and answering it off a refused read would hand\n * them schema defaults as though they were stored settings.\n */\nexport function useSitePluginConfig(\n orgId: string | null | undefined,\n hostId: string | null | undefined,\n pluginId: string,\n): { config: Record<string, unknown>; overrides: string[]; ready: boolean } {\n const firestore = useFirestore()\n const { data: org, status: orgStatus } = useFirestoreDoc<\n Record<string, unknown>\n >(\n () =>\n orgId ? doc(firestore, 'orgs', orgId, 'pluginSettings', pluginId) : null,\n [firestore, orgId, pluginId],\n )\n const { data: host, status: hostStatus } = useFirestoreDoc<\n Record<string, unknown>\n >(\n () =>\n hostId\n ? doc(firestore, 'hosts', hostId, 'pluginSettings', pluginId)\n : null,\n [firestore, hostId, pluginId],\n )\n const resolved = useMemo(() => {\n const schema = getPluginConfigSchema(pluginId)\n // Without a registered schema there is no field list to layer by, so the\n // site's raw document simply narrows the workspace's — the same degrade\n // `usePluginConfig` makes, one level deeper.\n if (!schema) {\n return {\n config: { ...(org ?? {}), ...(host ?? {}) },\n overrides: Object.keys(host ?? {}),\n }\n }\n return {\n config: resolvePluginConfig(schema, {\n org: org ?? null,\n host: host ?? null,\n }),\n overrides: pluginConfigOverrides(schema, host ?? null),\n }\n }, [pluginId, org, host])\n return {\n ...resolved,\n ready: orgStatus === 'success' && hostStatus === 'success',\n }\n}\n\nexport default usePluginConfig\n"],"names":["getPluginConfigSchema","mergePluginConfig","pluginConfigOverrides","resolvePluginConfig","doc","useMemo","useFirestore","useFirestoreDoc","usePluginConfig","orgId","pluginId","firestore","data","status","config","schema","ready","useSitePluginConfig","hostId","org","orgStatus","host","hostStatus","resolved","overrides","Object","keys"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,qBAAqB,EACrBC,iBAAiB,EACjBC,qBAAqB,EACrBC,mBAAmB,QACd,eAAc;AACrB,SAASC,GAAG,QAAQ,qBAAoB;AACxC,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,YAAY,QAAQ,kCAA8B;AAC3D,SAASC,eAAe,QAAQ,yBAAqB;AAErD;;;;;;CAMC,GACD,OAAO,SAASC,gBACdC,KAAgC,EAChCC,QAAgB;IAEhB,MAAMC,YAAYL;IAClB,4DAA4D;IAC5D,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,yEAAyE;IACzE,yEAAyE;IACzE,yBAAyB;IACzB,MAAM,EAAEM,IAAI,EAAEC,MAAM,EAAE,GAAGN,gBACvB,IACEE,QACIL,IAAIO,WAAW,QAAQF,OAAO,kBAAkBC,YAChD,MACN;QAACC;QAAWF;QAAOC;KAAS;IAE9B,MAAMI,SAAST,QAAQ;QACrB,MAAMU,SAASf,sBAAsBU;QACrC,IAAI,CAACK,QAAQ,OAAQH,eAAAA,OAAoC,CAAC;QAC1D,OAAOX,kBAAkBc,QAAQH,eAAAA,OAAQ;IAC3C,GAAG;QAACF;QAAUE;KAAK;IACnB,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,OAAO;QAAEE;QAAQE,OAAOH,WAAW;IAAU;AAC/C;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASI,oBACdR,KAAgC,EAChCS,MAAiC,EACjCR,QAAgB;IAEhB,MAAMC,YAAYL;IAClB,MAAM,EAAEM,MAAMO,GAAG,EAAEN,QAAQO,SAAS,EAAE,GAAGb,gBAGvC,IACEE,QAAQL,IAAIO,WAAW,QAAQF,OAAO,kBAAkBC,YAAY,MACtE;QAACC;QAAWF;QAAOC;KAAS;IAE9B,MAAM,EAAEE,MAAMS,IAAI,EAAER,QAAQS,UAAU,EAAE,GAAGf,gBAGzC,IACEW,SACId,IAAIO,WAAW,SAASO,QAAQ,kBAAkBR,YAClD,MACN;QAACC;QAAWO;QAAQR;KAAS;IAE/B,MAAMa,WAAWlB,QAAQ;QACvB,MAAMU,SAASf,sBAAsBU;QACrC,yEAAyE;QACzE,wEAAwE;QACxE,6CAA6C;QAC7C,IAAI,CAACK,QAAQ;YACX,OAAO;gBACLD,QAAQ,aAAMK,cAAAA,MAAO,CAAC,GAAQE,eAAAA,OAAQ,CAAC;gBACvCG,WAAWC,OAAOC,IAAI,CAACL,eAAAA,OAAQ,CAAC;YAClC;QACF;QACA,OAAO;YACLP,QAAQX,oBAAoBY,QAAQ;gBAClCI,GAAG,EAAEA,cAAAA,MAAO;gBACZE,IAAI,EAAEA,eAAAA,OAAQ;YAChB;YACAG,WAAWtB,sBAAsBa,QAAQM,eAAAA,OAAQ;QACnD;IACF,GAAG;QAACX;QAAUS;QAAKE;KAAK;IACxB,OAAO,aACFE;QACHP,OAAOI,cAAc,aAAaE,eAAe;;AAErD;AAEA,eAAed,gBAAe"}
@@ -0,0 +1,28 @@
1
+ export interface ScopeTokensState {
2
+ /** The caller's read set, for `array-contains-any` (AGL-1037). */
3
+ tokens: string[];
4
+ /** Org-wide members read everything and need no query filter. */
5
+ orgWide: boolean;
6
+ loaded: boolean;
7
+ }
8
+ /**
9
+ * The signed-in user's scope tokens in an org (AGL-1044/1045).
10
+ *
11
+ * This is NOT a UI-gating convenience — it is required for correctness.
12
+ * Under AGL-1041/1042 the rules evaluate per document on a LIST, and
13
+ * Firestore rejects the WHOLE query if any candidate would fail. So a
14
+ * scoped collaborator's unfiltered `collection(...)` read comes back
15
+ * `permission-denied`, not filtered: without the matching
16
+ * `array-contains-any` constraint the page does not show less, it shows
17
+ * an error.
18
+ *
19
+ * Org-wide members deliberately get `orgWide: true` and no filter. Adding
20
+ * one would cost them a composite index for no benefit, and would silently
21
+ * hide any doc whose `visibleTo` the backfill has not reached.
22
+ *
23
+ * Defaults to org-wide while loading: the rules are the enforcement point,
24
+ * and guessing "scoped" before the member doc arrives would flash an empty
25
+ * library at people who can see everything.
26
+ */
27
+ export declare function useScopeTokens(orgId: string | undefined): ScopeTokensState;
28
+ export default useScopeTokens;
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+ import { _ as _extends } from "@swc/helpers/_/_extends";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ import { isOrgWideMember, memberScopeTokens, ORG_SCOPE_TOKEN } from "@aglyn/aglyn";
19
+ import { doc, getDoc } from "firebase/firestore";
20
+ import { useEffect, useState } from "react";
21
+ import { useFirestore, useUser } from "./firebase/firebase-services.js";
22
+ /**
23
+ * The signed-in user's scope tokens in an org (AGL-1044/1045).
24
+ *
25
+ * This is NOT a UI-gating convenience — it is required for correctness.
26
+ * Under AGL-1041/1042 the rules evaluate per document on a LIST, and
27
+ * Firestore rejects the WHOLE query if any candidate would fail. So a
28
+ * scoped collaborator's unfiltered `collection(...)` read comes back
29
+ * `permission-denied`, not filtered: without the matching
30
+ * `array-contains-any` constraint the page does not show less, it shows
31
+ * an error.
32
+ *
33
+ * Org-wide members deliberately get `orgWide: true` and no filter. Adding
34
+ * one would cost them a composite index for no benefit, and would silently
35
+ * hide any doc whose `visibleTo` the backfill has not reached.
36
+ *
37
+ * Defaults to org-wide while loading: the rules are the enforcement point,
38
+ * and guessing "scoped" before the member doc arrives would flash an empty
39
+ * library at people who can see everything.
40
+ */ export function useScopeTokens(orgId) {
41
+ const { data: user } = useUser();
42
+ const firestore = useFirestore();
43
+ const [state, setState] = useState({
44
+ tokens: [
45
+ ORG_SCOPE_TOKEN
46
+ ],
47
+ orgWide: true,
48
+ loaded: false
49
+ });
50
+ useEffect(()=>{
51
+ const uid = user == null ? void 0 : user.uid;
52
+ if (!uid || !orgId) {
53
+ setState({
54
+ tokens: [
55
+ ORG_SCOPE_TOKEN
56
+ ],
57
+ orgWide: true,
58
+ loaded: Boolean(uid)
59
+ });
60
+ return;
61
+ }
62
+ let cancelled = false;
63
+ getDoc(doc(firestore, 'orgs', orgId, 'members', uid)).then((snapshot)=>{
64
+ if (cancelled) return;
65
+ const member = snapshot.exists() ? _extends({
66
+ $id: uid
67
+ }, snapshot.data()) : undefined;
68
+ // `memberScopeTokens` owns the "stored projection, else recompute"
69
+ // rule so the client and every Admin-SDK gate answer it the same
70
+ // way — a member doc the AGL-1040 backfill has not reached must
71
+ // still resolve to what `grantHostAccess` would have stamped.
72
+ setState({
73
+ tokens: memberScopeTokens(member),
74
+ orgWide: isOrgWideMember(member),
75
+ loaded: true
76
+ });
77
+ }).catch(()=>{
78
+ if (!cancelled) {
79
+ setState({
80
+ tokens: [
81
+ ORG_SCOPE_TOKEN
82
+ ],
83
+ orgWide: true,
84
+ loaded: true
85
+ });
86
+ }
87
+ });
88
+ return ()=>{
89
+ cancelled = true;
90
+ };
91
+ }, [
92
+ firestore,
93
+ orgId,
94
+ user
95
+ ]);
96
+ return state;
97
+ }
98
+ export default useScopeTokens;
99
+
100
+ //# sourceMappingURL=use-scope-tokens.js.map
@@ -0,0 +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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type * as Aglyn from '@aglyn/aglyn';
18
+ import type { DocumentReference } from 'firebase/firestore';
19
+ import { type FirestoreDocOptions } from './firebase/firebase-services';
20
+ export declare const useScreenVersionRef: ({ hostId, screenId, versionId }: {
21
+ hostId: string;
22
+ screenId: string;
23
+ versionId: string;
24
+ }) => DocumentReference<Aglyn.AglynScreenVersion>;
25
+ export declare const useScreenVersion: (data: {
26
+ screenId: string;
27
+ versionId: string;
28
+ hostId: string;
29
+ }, options?: FirestoreDocOptions<Aglyn.AglynScreenVersion>) => {
30
+ doc: import("./firebase/firebase-services").ObservableStatus<Aglyn.AglynScreenVersion>;
31
+ setDoc: import("./helpers/use-modify-doc-callback").ModifyDocCallback<Aglyn.AglynScreenVersion>;
32
+ };
33
+ export default useScreenVersion;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ // A TYPE-only namespace: every `Aglyn.` reference below is a type, and held
17
+ // as a VALUE the namespace pins every module the barrel reaches into the
18
+ // console shell that renders these hooks. `import type` erases instead.
19
+ import { _ as _extends } from "@swc/helpers/_/_extends";
20
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
21
+ import { compress, decompress } from "@aglyn/aglyn";
22
+ import { Timestamp } from "@aglyn/shared-util-timestamp";
23
+ import { Bytes, doc } from "firebase/firestore";
24
+ import { useFirestore } from "./firebase/firebase-services.js";
25
+ import useDoc from "./helpers/use-doc.js";
26
+ export const useScreenVersionRef = ({ hostId, screenId, versionId })=>{
27
+ const firestore = useFirestore();
28
+ const ref = doc(firestore, 'hosts', hostId, 'screens', screenId, 'versions', versionId);
29
+ return ref.withConverter({
30
+ toFirestore (data) {
31
+ const { $id } = data, rest = _object_without_properties_loose(data, [
32
+ "$id"
33
+ ]);
34
+ // Only emit `nodes` when the write actually carries them (AGL-1250).
35
+ // Compressing `rest?.nodes || {}` unconditionally meant a partial
36
+ // `setDoc(…, {merge: true})` — one that set some other field —
37
+ // shipped an EMPTY compressed map, and merge faithfully merged that
38
+ // emptiness over the real tree, silently destroying the document.
39
+ if ((rest == null ? void 0 : rest.nodes) === undefined) return _extends({}, rest, {
40
+ updatedAt: Timestamp.now()
41
+ });
42
+ const nodes = rest.nodes instanceof Bytes ? rest.nodes : Bytes.fromUint8Array(compress(rest.nodes));
43
+ return _extends({}, rest, {
44
+ nodes,
45
+ updatedAt: Timestamp.now()
46
+ });
47
+ },
48
+ fromFirestore (snapshot, options) {
49
+ if (!snapshot.exists()) return undefined;
50
+ const data = snapshot.data(options);
51
+ if ((data == null ? void 0 : data.nodes) instanceof Bytes) {
52
+ return _extends({}, data, {
53
+ nodes: decompress(data.nodes)
54
+ });
55
+ }
56
+ return data;
57
+ }
58
+ });
59
+ };
60
+ export const useScreenVersion = (data, options)=>{
61
+ return useDoc(useScreenVersionRef(data), options);
62
+ };
63
+ export default useScreenVersion;
64
+
65
+ //# sourceMappingURL=use-screen-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-screen-version.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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// A TYPE-only namespace: every `Aglyn.` reference below is a type, and held\n// as a VALUE the namespace pins every module the barrel reaches into the\n// console shell that renders these hooks. `import type` erases instead.\nimport type * as Aglyn from '@aglyn/aglyn'\nimport { compress, decompress } from '@aglyn/aglyn'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport type { DocumentReference } from 'firebase/firestore'\nimport { Bytes, doc } from 'firebase/firestore'\nimport { useFirestore, type FirestoreDocOptions } from './firebase/firebase-services'\nimport useDoc from './helpers/use-doc'\n\nexport const useScreenVersionRef = ({ hostId, screenId, versionId }: { hostId: string; screenId: string; versionId: string }) => {\n const firestore = useFirestore()\n const ref = doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n screenId,\n 'versions',\n versionId,\n )\n return ref.withConverter({\n toFirestore(data) {\n const { $id, ...rest } = data\n // Only emit `nodes` when the write actually carries them (AGL-1250).\n // Compressing `rest?.nodes || {}` unconditionally meant a partial\n // `setDoc(…, {merge: true})` — one that set some other field —\n // shipped an EMPTY compressed map, and merge faithfully merged that\n // emptiness over the real tree, silently destroying the document.\n if (rest?.nodes === undefined) return { ...rest, updatedAt: Timestamp.now() }\n const nodes = rest.nodes instanceof Bytes\n ? rest.nodes\n : Bytes.fromUint8Array(compress(rest.nodes))\n return { ...rest, nodes, updatedAt: Timestamp.now() }\n },\n fromFirestore(snapshot, options) {\n if (!snapshot.exists()) return undefined\n const data = snapshot.data(options)\n if (data?.nodes instanceof Bytes) {\n return { ...data, nodes: decompress(data.nodes) } as Aglyn.AglynScreenVersion\n }\n return data as Aglyn.AglynScreenVersion\n },\n }) as DocumentReference<Aglyn.AglynScreenVersion>\n}\n\nexport const useScreenVersion = (\n data: {\n screenId: string\n versionId: string\n hostId: string\n },\n options?: FirestoreDocOptions<Aglyn.AglynScreenVersion>,\n) => {\n return useDoc(useScreenVersionRef(data), options)\n}\n\nexport default useScreenVersion\n"],"names":["compress","decompress","Timestamp","Bytes","doc","useFirestore","useDoc","useScreenVersionRef","hostId","screenId","versionId","firestore","ref","withConverter","toFirestore","data","$id","rest","nodes","undefined","updatedAt","now","fromUint8Array","fromFirestore","snapshot","options","exists","useScreenVersion"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,4EAA4E;AAC5E,yEAAyE;AACzE,wEAAwE;;;AAExE,SAASA,QAAQ,EAAEC,UAAU,QAAQ,eAAc;AACnD,SAASC,SAAS,QAAQ,+BAA8B;AAExD,SAASC,KAAK,EAAEC,GAAG,QAAQ,qBAAoB;AAC/C,SAASC,YAAY,QAAkC,kCAA8B;AACrF,OAAOC,YAAY,uBAAmB;AAEtC,OAAO,MAAMC,sBAAsB,CAAC,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,SAAS,EAA2D;IAC1H,MAAMC,YAAYN;IAClB,MAAMO,MAAMR,IACVO,WACA,SACAH,QACA,WACAC,UACA,YACAC;IAEF,OAAOE,IAAIC,aAAa,CAAC;QACvBC,aAAYC,IAAI;YACd,MAAM,EAAEC,GAAG,EAAW,GAAGD,MAATE,wCAASF;;;YACzB,qEAAqE;YACrE,kEAAkE;YAClE,+DAA+D;YAC/D,oEAAoE;YACpE,kEAAkE;YAClE,IAAIE,CAAAA,wBAAAA,KAAMC,KAAK,MAAKC,WAAW,OAAO,aAAKF;gBAAMG,WAAWlB,UAAUmB,GAAG;;YACzE,MAAMH,QAAQD,KAAKC,KAAK,YAAYf,QAChCc,KAAKC,KAAK,GACVf,MAAMmB,cAAc,CAACtB,SAASiB,KAAKC,KAAK;YAC5C,OAAO,aAAKD;gBAAMC;gBAAOE,WAAWlB,UAAUmB,GAAG;;QACnD;QACAE,eAAcC,QAAQ,EAAEC,OAAO;YAC7B,IAAI,CAACD,SAASE,MAAM,IAAI,OAAOP;YAC/B,MAAMJ,OAAOS,SAAST,IAAI,CAACU;YAC3B,IAAIV,CAAAA,wBAAAA,KAAMG,KAAK,aAAYf,OAAO;gBAChC,OAAO,aAAKY;oBAAMG,OAAOjB,WAAWc,KAAKG,KAAK;;YAChD;YACA,OAAOH;QACT;IACF;AACF,EAAC;AAED,OAAO,MAAMY,mBAAmB,CAC9BZ,MAKAU;IAEA,OAAOnB,OAAOC,oBAAoBQ,OAAOU;AAC3C,EAAC;AAED,eAAeE,iBAAgB"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type * as Aglyn from '@aglyn/aglyn';
18
+ import { Timestamp } from '@aglyn/shared-util-timestamp';
19
+ import { type FirestoreDocOptions } from './firebase/firebase-services';
20
+ export declare const useScreenRef: ({ hostId, screenId }: {
21
+ hostId: Aglyn.HostUid;
22
+ screenId: Aglyn.ScreenUid;
23
+ }) => import("@firebase/firestore").DocumentReference<Aglyn.AglynScreen, {
24
+ updatedAt: Timestamp;
25
+ hostId?: Aglyn.HostUid;
26
+ parentId?: Aglyn.ScreenUid;
27
+ slug?: Aglyn.ScreenSlug;
28
+ order?: number;
29
+ versionId?: Aglyn.VersionUid;
30
+ publishSchedule?: Aglyn.PublishSchedule;
31
+ protection?: {
32
+ passwordHash?: string;
33
+ };
34
+ locale?: string;
35
+ localeVariants?: Record<string, Aglyn.ScreenUid>;
36
+ status?: Aglyn.HostScreenStatus;
37
+ createdAt?: import("@aglyn/shared-util-timestamp").ITimestamp;
38
+ publishedAt?: import("@aglyn/shared-util-timestamp").ITimestamp;
39
+ deletedAt?: import("@aglyn/shared-util-timestamp").ITimestamp;
40
+ displayName?: string;
41
+ nameLower?: string;
42
+ description?: string;
43
+ seo?: {
44
+ title?: string;
45
+ description?: string;
46
+ breadcrumb?: string;
47
+ image?: string;
48
+ imageWidth?: number;
49
+ imageHeight?: number;
50
+ imageAlt?: string;
51
+ };
52
+ schedule?: {
53
+ startAt?: import("@aglyn/shared-util-timestamp").ITimestamp;
54
+ endAt?: import("@aglyn/shared-util-timestamp").ITimestamp;
55
+ next?: Aglyn.VersionUid;
56
+ previous?: Aglyn.VersionUid;
57
+ };
58
+ visibility?: Aglyn.HostScreenVisibility;
59
+ layoutId?: Aglyn.LayoutUid;
60
+ }>;
61
+ export declare const useScreen: (data: {
62
+ hostId: Aglyn.HostUid;
63
+ screenId: Aglyn.ScreenUid;
64
+ }, options?: FirestoreDocOptions<Aglyn.AglynScreen>) => {
65
+ doc: import("./firebase/firebase-services").ObservableStatus<Aglyn.AglynScreen>;
66
+ setDoc: import("./helpers/use-modify-doc-callback").ModifyDocCallback<Aglyn.AglynScreen>;
67
+ };
68
+ export default useScreen;