@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,93 @@
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 { collection } from "firebase/firestore";
18
+ import { useMemo } from "react";
19
+ import { collectionCeiling } from "./host-collection-queries.js";
20
+ import { useFirestore } from "./firebase/firebase-services.js";
21
+ import { useFirestoreCollection } from "./use-firestore-collection.js";
22
+ /**
23
+ * How many campaigns a picker offers.
24
+ *
25
+ * The same fifty the campaigns table draws, so a campaign a merchant can see
26
+ * in the list is a campaign they can file a record under. A picker with a
27
+ * larger window would offer campaigns the list they came from does not show;
28
+ * a smaller one would hide campaigns that are plainly there.
29
+ */ export const HOST_CAMPAIGN_CEILING = 50;
30
+ /**
31
+ * THE SITE'S CAMPAIGNS, for a picker on some other record's page.
32
+ *
33
+ * Lives here rather than in the marketing plugin because the console app may
34
+ * not import a feature plugin — a screen's detail page is an app route — and
35
+ * the form and contact surfaces that need the same list are two other
36
+ * libraries again. One read, one shape, three callers.
37
+ *
38
+ * ## It is OFF unless a caller asks
39
+ *
40
+ * `enabled` defaults to false. The picker this feeds sits on a record's own
41
+ * page beside fields a reader came for; charging every open of a form, a
42
+ * screen or a contact for a hundred campaign documents they may not touch is
43
+ * the read-on-mount this console refuses. The callers turn it on when their
44
+ * editing surface opens.
45
+ *
46
+ * ## Ordered on the document name
47
+ *
48
+ * `collectionCeiling`, for the reason it exists: `startAtMs` and `endAtMs`
49
+ * are both optional on a campaign — an open-ended one carries neither — so
50
+ * ordering on a date would not mis-sort the picker, it would DROP the
51
+ * campaigns that have no dates. The labels are sorted by name here, over the
52
+ * whole ceiling rather than a slice of it, which is the one case sorting a
53
+ * window is honest.
54
+ */ export function useHostCampaigns(hostId, options) {
55
+ var _ref;
56
+ const enabled = (_ref = options == null ? void 0 : options.enabled) != null ? _ref : false;
57
+ const firestore = useFirestore();
58
+ const { data, status } = useFirestoreCollection(()=>enabled && hostId ? collectionCeiling(collection(firestore, 'hosts', hostId, 'emailCampaigns'), HOST_CAMPAIGN_CEILING) : null, [
59
+ firestore,
60
+ hostId,
61
+ enabled
62
+ ], {
63
+ idField: '$id'
64
+ });
65
+ return useMemo(()=>{
66
+ const rows = data != null ? data : [];
67
+ const truncated = rows.length > HOST_CAMPAIGN_CEILING;
68
+ const live = rows.slice(0, HOST_CAMPAIGN_CEILING)// A campaign the console soft-deleted is not a campaign a record may be
69
+ // filed under. The campaigns table filters the same field for the same
70
+ // reason.
71
+ .filter((row)=>!row['deletedAt']);
72
+ return {
73
+ options: live.map((row)=>{
74
+ var _row_name, _row_startAtMs, _row_endAtMs;
75
+ return {
76
+ value: String(row['$id']),
77
+ label: String((_row_name = row['name']) != null ? _row_name : row['$id']),
78
+ startAtMs: (_row_startAtMs = row['startAtMs']) != null ? _row_startAtMs : null,
79
+ endAtMs: (_row_endAtMs = row['endAtMs']) != null ? _row_endAtMs : null
80
+ };
81
+ }).sort((a, b)=>a.label.localeCompare(b.label)),
82
+ truncated,
83
+ ready: enabled && status !== 'loading'
84
+ };
85
+ }, [
86
+ data,
87
+ enabled,
88
+ status
89
+ ]);
90
+ }
91
+ export default useHostCampaigns;
92
+
93
+ //# sourceMappingURL=use-host-campaigns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-host-campaigns.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 { collection } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { collectionCeiling } from './host-collection-queries'\nimport { useFirestore } from './firebase/firebase-services'\nimport { useFirestoreCollection } from './use-firestore-collection'\n\n/**\n * How many campaigns a picker offers.\n *\n * The same fifty the campaigns table draws, so a campaign a merchant can see\n * in the list is a campaign they can file a record under. A picker with a\n * larger window would offer campaigns the list they came from does not show;\n * a smaller one would hide campaigns that are plainly there.\n */\nexport const HOST_CAMPAIGN_CEILING = 50\n\n/** One campaign, as much of it as a picker needs. */\nexport interface HostCampaignOption {\n value: string\n label: string\n /** The campaign window, for a caller that wants to say when it ran. */\n startAtMs?: number | null\n endAtMs?: number | null\n}\n\nexport interface HostCampaigns {\n options: HostCampaignOption[]\n /** The site holds more campaigns than the ceiling offers. */\n truncated: boolean\n /** The read has answered — false while it is still settling or disabled. */\n ready: boolean\n}\n\n/**\n * THE SITE'S CAMPAIGNS, for a picker on some other record's page.\n *\n * Lives here rather than in the marketing plugin because the console app may\n * not import a feature plugin — a screen's detail page is an app route — and\n * the form and contact surfaces that need the same list are two other\n * libraries again. One read, one shape, three callers.\n *\n * ## It is OFF unless a caller asks\n *\n * `enabled` defaults to false. The picker this feeds sits on a record's own\n * page beside fields a reader came for; charging every open of a form, a\n * screen or a contact for a hundred campaign documents they may not touch is\n * the read-on-mount this console refuses. The callers turn it on when their\n * editing surface opens.\n *\n * ## Ordered on the document name\n *\n * `collectionCeiling`, for the reason it exists: `startAtMs` and `endAtMs`\n * are both optional on a campaign — an open-ended one carries neither — so\n * ordering on a date would not mis-sort the picker, it would DROP the\n * campaigns that have no dates. The labels are sorted by name here, over the\n * whole ceiling rather than a slice of it, which is the one case sorting a\n * window is honest.\n */\nexport function useHostCampaigns(\n hostId: string | undefined,\n options?: { enabled?: boolean },\n): HostCampaigns {\n const enabled = options?.enabled ?? false\n const firestore = useFirestore()\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && hostId\n ? collectionCeiling(\n collection(firestore, 'hosts', hostId, 'emailCampaigns'),\n HOST_CAMPAIGN_CEILING,\n )\n : null,\n [firestore, hostId, enabled],\n { idField: '$id' },\n )\n\n return useMemo(() => {\n const rows = data ?? []\n const truncated = rows.length > HOST_CAMPAIGN_CEILING\n const live = rows\n .slice(0, HOST_CAMPAIGN_CEILING)\n // A campaign the console soft-deleted is not a campaign a record may be\n // filed under. The campaigns table filters the same field for the same\n // reason.\n .filter((row) => !row['deletedAt'])\n return {\n options: live\n .map((row) => ({\n value: String(row['$id']),\n label: String(row['name'] ?? row['$id']),\n startAtMs: (row['startAtMs'] as number | null | undefined) ?? null,\n endAtMs: (row['endAtMs'] as number | null | undefined) ?? null,\n }))\n .sort((a, b) => a.label.localeCompare(b.label)),\n truncated,\n ready: enabled && status !== 'loading',\n }\n }, [data, enabled, status])\n}\n\nexport default useHostCampaigns\n"],"names":["collection","useMemo","collectionCeiling","useFirestore","useFirestoreCollection","HOST_CAMPAIGN_CEILING","useHostCampaigns","hostId","options","enabled","firestore","data","status","idField","rows","truncated","length","live","slice","filter","row","map","value","String","label","startAtMs","endAtMs","sort","a","b","localeCompare","ready"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,UAAU,QAAQ,qBAAoB;AAC/C,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,iBAAiB,QAAQ,+BAA2B;AAC7D,SAASC,YAAY,QAAQ,kCAA8B;AAC3D,SAASC,sBAAsB,QAAQ,gCAA4B;AAEnE;;;;;;;CAOC,GACD,OAAO,MAAMC,wBAAwB,GAAE;AAmBvC;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,SAASC,iBACdC,MAA0B,EAC1BC,OAA+B;;IAE/B,MAAMC,kBAAUD,2BAAAA,QAASC,OAAO,mBAAI;IACpC,MAAMC,YAAYP;IAClB,MAAM,EAAEQ,IAAI,EAAEC,MAAM,EAAE,GAAGR,uBACvB,IACEK,WAAWF,SACPL,kBACEF,WAAWU,WAAW,SAASH,QAAQ,mBACvCF,yBAEF,MACN;QAACK;QAAWH;QAAQE;KAAQ,EAC5B;QAAEI,SAAS;IAAM;IAGnB,OAAOZ,QAAQ;QACb,MAAMa,OAAOH,eAAAA,OAAQ,EAAE;QACvB,MAAMI,YAAYD,KAAKE,MAAM,GAAGX;QAChC,MAAMY,OAAOH,KACVI,KAAK,CAAC,GAAGb,sBACV,wEAAwE;QACxE,uEAAuE;QACvE,UAAU;SACTc,MAAM,CAAC,CAACC,MAAQ,CAACA,GAAG,CAAC,YAAY;QACpC,OAAO;YACLZ,SAASS,KACNI,GAAG,CAAC,CAACD;oBAEUA,WACFA,gBACFA;uBAJG;oBACbE,OAAOC,OAAOH,GAAG,CAAC,MAAM;oBACxBI,OAAOD,QAAOH,YAAAA,GAAG,CAAC,OAAO,YAAXA,YAAeA,GAAG,CAAC,MAAM;oBACvCK,SAAS,GAAGL,iBAAAA,GAAG,CAAC,YAAY,YAAhBA,iBAAkD;oBAC9DM,OAAO,GAAGN,eAAAA,GAAG,CAAC,UAAU,YAAdA,eAAgD;gBAC5D;eACCO,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEJ,KAAK,CAACM,aAAa,CAACD,EAAEL,KAAK;YAC/CT;YACAgB,OAAOtB,WAAWG,WAAW;QAC/B;IACF,GAAG;QAACD;QAAMF;QAASG;KAAO;AAC5B;AAEA,eAAeN,iBAAgB"}
@@ -0,0 +1,96 @@
1
+ export interface HostOrgIdState {
2
+ /** The owning org, or null once we know there isn't one. */
3
+ orgId: string | null;
4
+ /** False until the `hostIndex` lookup has settled. */
5
+ loaded: boolean;
6
+ }
7
+ /**
8
+ * The org a host belongs to, plus whether we know yet (AGL-1061).
9
+ *
10
+ * `useHostOrgId` below returns `null` for THREE different situations —
11
+ * still loading, no owning org, and lookup failed — and callers that
12
+ * branch on truthiness take the no-org branch during the first render or
13
+ * two of every mount, while the `hostIndex` `getDoc` is in flight.
14
+ *
15
+ * For a read that is a harmless flash of an empty list. For a WRITE it is
16
+ * not: a `setDoc` resolving inside that window lands under
17
+ * `hosts/{hostId}/…`, which AGL-1050 left with no readers — so the save
18
+ * appears to succeed and the row is unreachable from anywhere afterwards.
19
+ *
20
+ * Anything that can write must therefore gate on `loaded` and hold rather
21
+ * than guess.
22
+ */
23
+ export declare function useHostOrgIdState(hostId: string | undefined): HostOrgIdState;
24
+ /**
25
+ * The org a host belongs to, resolved from the `hostIndex` mirror
26
+ * (AGL-237). Host-scoped pages use this — NOT the workspace context — so
27
+ * org-shared data stays correct when a multi-org user deep-links into a
28
+ * host outside their selected workspace.
29
+ *
30
+ * Null while loading AND for pre-org hosts, which callers cannot tell
31
+ * apart. Fine for the many callers that only ever build an org path from
32
+ * it and render nothing until it arrives; anything that FALLS BACK to a
33
+ * host path — and especially anything that writes — wants
34
+ * {@link useHostOrgIdState} instead (AGL-1061).
35
+ */
36
+ export declare function useHostOrgId(hostId: string | undefined): string | null;
37
+ export interface OrgDataScope {
38
+ /**
39
+ * Firestore parent for org-shared data: `['orgs', orgId]`, or NULL when
40
+ * there is no such path to build — the lookup is still in flight, or the
41
+ * host has no owning org. Callers pass `null` straight through to their
42
+ * query factory, which issues nothing.
43
+ *
44
+ * There is deliberately no `['hosts', hostId]` alternative (AGL-1050).
45
+ */
46
+ scope: readonly ['orgs', string] | null;
47
+ /** The owning org once known, else null. */
48
+ orgId: string | null;
49
+ /**
50
+ * Whether the lookup has SETTLED. Distinguishes the two reasons `scope`
51
+ * is null: false means "not yet, keep the spinner", true means "there is
52
+ * no org and none is coming" — render the empty state, leave saves off.
53
+ */
54
+ ready: boolean;
55
+ }
56
+ /**
57
+ * The `['orgs', orgId]` parent every host-scoped card builds, with an
58
+ * honest `ready` flag (AGL-1061).
59
+ *
60
+ * This existed as a copy-pasted ternary in nine components:
61
+ *
62
+ * ```ts
63
+ * const orgId = props.orgId ?? useHostOrgId(hostId)
64
+ * const scope = orgId ? ['orgs', orgId] : ['hosts', hostId]
65
+ * ```
66
+ *
67
+ * which silently resolves to the HOST branch for the first render or two
68
+ * of every mount, because `useHostOrgId` cannot say "not yet". Centralising
69
+ * it means the window is handled once, correctly, instead of nine times
70
+ * from memory.
71
+ *
72
+ * ## Why there is no host fallback any more (AGL-1050)
73
+ *
74
+ * That `: ['hosts', hostId]` branch was the pre-AGL-237 storage location,
75
+ * kept as a migration path. AGL-1061 counted what actually lives there in
76
+ * production — `datasets`, `lists`, `contacts`, `contactSegments`, exact
77
+ * count() per host, no truncation — and the answer was 0 across every
78
+ * host. So it was not a migration path; it was a second address for
79
+ * org-shared data that nothing had ever written and nothing read back.
80
+ *
81
+ * Keeping it ready-gated would have been enough to close the live defect.
82
+ * It was removed instead because the branch could only ever be reached by
83
+ * a bug — most sharply through the `hostIndex` lookup's `.catch()`, where
84
+ * a transient failure used to hand back a confident host path and let the
85
+ * card save a row nobody could reach afterwards. A null scope cannot do
86
+ * that: the query is never issued and the save is never enabled.
87
+ *
88
+ * The rules now deny `hosts/{hostId}/datasets/**` outright, so the path is
89
+ * closed on both sides rather than merely unused on one.
90
+ */
91
+ export declare function useOrgDataScope(options: {
92
+ hostId?: string | undefined;
93
+ /** An explicit org skips the lookup entirely and is ready immediately. */
94
+ orgId?: string | undefined;
95
+ }): OrgDataScope;
96
+ export default useHostOrgId;
@@ -0,0 +1,154 @@
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 { doc, getDoc } from "firebase/firestore";
18
+ import { useEffect, useMemo, useState } from "react";
19
+ import { useFirestore } from "./firebase/firebase-services.js";
20
+ /**
21
+ * The org a host belongs to, plus whether we know yet (AGL-1061).
22
+ *
23
+ * `useHostOrgId` below returns `null` for THREE different situations —
24
+ * still loading, no owning org, and lookup failed — and callers that
25
+ * branch on truthiness take the no-org branch during the first render or
26
+ * two of every mount, while the `hostIndex` `getDoc` is in flight.
27
+ *
28
+ * For a read that is a harmless flash of an empty list. For a WRITE it is
29
+ * not: a `setDoc` resolving inside that window lands under
30
+ * `hosts/{hostId}/…`, which AGL-1050 left with no readers — so the save
31
+ * appears to succeed and the row is unreachable from anywhere afterwards.
32
+ *
33
+ * Anything that can write must therefore gate on `loaded` and hold rather
34
+ * than guess.
35
+ */ export function useHostOrgIdState(hostId) {
36
+ const firestore = useFirestore();
37
+ const [state, setState] = useState({
38
+ orgId: null,
39
+ loaded: false
40
+ });
41
+ useEffect(()=>{
42
+ // No host to resolve is a SETTLED answer, not a pending one — the org
43
+ // Data and Media pages pass no host and must not wait forever.
44
+ if (!hostId) {
45
+ setState({
46
+ orgId: null,
47
+ loaded: true
48
+ });
49
+ return undefined;
50
+ }
51
+ let active = true;
52
+ setState({
53
+ orgId: null,
54
+ loaded: false
55
+ });
56
+ void getDoc(doc(firestore, 'hostIndex', hostId)).then((snapshot)=>{
57
+ if (active) {
58
+ var _ref;
59
+ var _snapshot_data;
60
+ setState({
61
+ orgId: (_ref = (_snapshot_data = snapshot.data()) == null ? void 0 : _snapshot_data['orgId']) != null ? _ref : null,
62
+ loaded: true
63
+ });
64
+ }
65
+ }).catch(()=>{
66
+ // A failed lookup is also settled. Retrying forever would leave
67
+ // the page permanently disabled; the host branch is the correct
68
+ // (and, per AGL-1050, empty) fallback.
69
+ if (active) setState({
70
+ orgId: null,
71
+ loaded: true
72
+ });
73
+ });
74
+ return ()=>{
75
+ active = false;
76
+ };
77
+ }, [
78
+ firestore,
79
+ hostId
80
+ ]);
81
+ return state;
82
+ }
83
+ /**
84
+ * The org a host belongs to, resolved from the `hostIndex` mirror
85
+ * (AGL-237). Host-scoped pages use this — NOT the workspace context — so
86
+ * org-shared data stays correct when a multi-org user deep-links into a
87
+ * host outside their selected workspace.
88
+ *
89
+ * Null while loading AND for pre-org hosts, which callers cannot tell
90
+ * apart. Fine for the many callers that only ever build an org path from
91
+ * it and render nothing until it arrives; anything that FALLS BACK to a
92
+ * host path — and especially anything that writes — wants
93
+ * {@link useHostOrgIdState} instead (AGL-1061).
94
+ */ export function useHostOrgId(hostId) {
95
+ return useHostOrgIdState(hostId).orgId;
96
+ }
97
+ /**
98
+ * The `['orgs', orgId]` parent every host-scoped card builds, with an
99
+ * honest `ready` flag (AGL-1061).
100
+ *
101
+ * This existed as a copy-pasted ternary in nine components:
102
+ *
103
+ * ```ts
104
+ * const orgId = props.orgId ?? useHostOrgId(hostId)
105
+ * const scope = orgId ? ['orgs', orgId] : ['hosts', hostId]
106
+ * ```
107
+ *
108
+ * which silently resolves to the HOST branch for the first render or two
109
+ * of every mount, because `useHostOrgId` cannot say "not yet". Centralising
110
+ * it means the window is handled once, correctly, instead of nine times
111
+ * from memory.
112
+ *
113
+ * ## Why there is no host fallback any more (AGL-1050)
114
+ *
115
+ * That `: ['hosts', hostId]` branch was the pre-AGL-237 storage location,
116
+ * kept as a migration path. AGL-1061 counted what actually lives there in
117
+ * production — `datasets`, `lists`, `contacts`, `contactSegments`, exact
118
+ * count() per host, no truncation — and the answer was 0 across every
119
+ * host. So it was not a migration path; it was a second address for
120
+ * org-shared data that nothing had ever written and nothing read back.
121
+ *
122
+ * Keeping it ready-gated would have been enough to close the live defect.
123
+ * It was removed instead because the branch could only ever be reached by
124
+ * a bug — most sharply through the `hostIndex` lookup's `.catch()`, where
125
+ * a transient failure used to hand back a confident host path and let the
126
+ * card save a row nobody could reach afterwards. A null scope cannot do
127
+ * that: the query is never issued and the save is never enabled.
128
+ *
129
+ * The rules now deny `hosts/{hostId}/datasets/**` outright, so the path is
130
+ * closed on both sides rather than merely unused on one.
131
+ */ export function useOrgDataScope(options) {
132
+ const { hostId, orgId: explicitOrgId } = options;
133
+ const state = useHostOrgIdState(explicitOrgId ? undefined : hostId);
134
+ const orgId = explicitOrgId != null ? explicitOrgId : state.orgId;
135
+ const loaded = Boolean(explicitOrgId) || state.loaded;
136
+ // Memoised because `scope` is a QUERY DEPENDENCY. A fresh tuple every
137
+ // render, passed to `useFirestoreCollection`, tears down and reopens the
138
+ // listener (and clears its data) on every render — so hand out a stable
139
+ // reference rather than trusting every caller to destructure primitives.
140
+ return useMemo(()=>({
141
+ orgId,
142
+ ready: loaded,
143
+ scope: orgId ? [
144
+ 'orgs',
145
+ orgId
146
+ ] : null
147
+ }), [
148
+ orgId,
149
+ loaded
150
+ ]);
151
+ }
152
+ export default useHostOrgId;
153
+
154
+ //# sourceMappingURL=use-host-org-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-host-org-id.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 { doc, getDoc } from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useFirestore } from './firebase/firebase-services'\n\nexport interface HostOrgIdState {\n /** The owning org, or null once we know there isn't one. */\n orgId: string | null\n /** False until the `hostIndex` lookup has settled. */\n loaded: boolean\n}\n\n/**\n * The org a host belongs to, plus whether we know yet (AGL-1061).\n *\n * `useHostOrgId` below returns `null` for THREE different situations —\n * still loading, no owning org, and lookup failed — and callers that\n * branch on truthiness take the no-org branch during the first render or\n * two of every mount, while the `hostIndex` `getDoc` is in flight.\n *\n * For a read that is a harmless flash of an empty list. For a WRITE it is\n * not: a `setDoc` resolving inside that window lands under\n * `hosts/{hostId}/…`, which AGL-1050 left with no readers — so the save\n * appears to succeed and the row is unreachable from anywhere afterwards.\n *\n * Anything that can write must therefore gate on `loaded` and hold rather\n * than guess.\n */\nexport function useHostOrgIdState(\n hostId: string | undefined,\n): HostOrgIdState {\n const firestore = useFirestore()\n const [state, setState] = useState<HostOrgIdState>({\n orgId: null,\n loaded: false,\n })\n\n useEffect(() => {\n // No host to resolve is a SETTLED answer, not a pending one — the org\n // Data and Media pages pass no host and must not wait forever.\n if (!hostId) {\n setState({ orgId: null, loaded: true })\n return undefined\n }\n let active = true\n setState({ orgId: null, loaded: false })\n void getDoc(doc(firestore, 'hostIndex', hostId))\n .then((snapshot) => {\n if (active) {\n setState({\n orgId: (snapshot.data()?.['orgId'] as string) ?? null,\n loaded: true,\n })\n }\n })\n .catch(() => {\n // A failed lookup is also settled. Retrying forever would leave\n // the page permanently disabled; the host branch is the correct\n // (and, per AGL-1050, empty) fallback.\n if (active) setState({ orgId: null, loaded: true })\n })\n return () => {\n active = false\n }\n }, [firestore, hostId])\n\n return state\n}\n\n/**\n * The org a host belongs to, resolved from the `hostIndex` mirror\n * (AGL-237). Host-scoped pages use this — NOT the workspace context — so\n * org-shared data stays correct when a multi-org user deep-links into a\n * host outside their selected workspace.\n *\n * Null while loading AND for pre-org hosts, which callers cannot tell\n * apart. Fine for the many callers that only ever build an org path from\n * it and render nothing until it arrives; anything that FALLS BACK to a\n * host path — and especially anything that writes — wants\n * {@link useHostOrgIdState} instead (AGL-1061).\n */\nexport function useHostOrgId(hostId: string | undefined): string | null {\n return useHostOrgIdState(hostId).orgId\n}\n\nexport interface OrgDataScope {\n /**\n * Firestore parent for org-shared data: `['orgs', orgId]`, or NULL when\n * there is no such path to build — the lookup is still in flight, or the\n * host has no owning org. Callers pass `null` straight through to their\n * query factory, which issues nothing.\n *\n * There is deliberately no `['hosts', hostId]` alternative (AGL-1050).\n */\n scope: readonly ['orgs', string] | null\n /** The owning org once known, else null. */\n orgId: string | null\n /**\n * Whether the lookup has SETTLED. Distinguishes the two reasons `scope`\n * is null: false means \"not yet, keep the spinner\", true means \"there is\n * no org and none is coming\" — render the empty state, leave saves off.\n */\n ready: boolean\n}\n\n/**\n * The `['orgs', orgId]` parent every host-scoped card builds, with an\n * honest `ready` flag (AGL-1061).\n *\n * This existed as a copy-pasted ternary in nine components:\n *\n * ```ts\n * const orgId = props.orgId ?? useHostOrgId(hostId)\n * const scope = orgId ? ['orgs', orgId] : ['hosts', hostId]\n * ```\n *\n * which silently resolves to the HOST branch for the first render or two\n * of every mount, because `useHostOrgId` cannot say \"not yet\". Centralising\n * it means the window is handled once, correctly, instead of nine times\n * from memory.\n *\n * ## Why there is no host fallback any more (AGL-1050)\n *\n * That `: ['hosts', hostId]` branch was the pre-AGL-237 storage location,\n * kept as a migration path. AGL-1061 counted what actually lives there in\n * production — `datasets`, `lists`, `contacts`, `contactSegments`, exact\n * count() per host, no truncation — and the answer was 0 across every\n * host. So it was not a migration path; it was a second address for\n * org-shared data that nothing had ever written and nothing read back.\n *\n * Keeping it ready-gated would have been enough to close the live defect.\n * It was removed instead because the branch could only ever be reached by\n * a bug — most sharply through the `hostIndex` lookup's `.catch()`, where\n * a transient failure used to hand back a confident host path and let the\n * card save a row nobody could reach afterwards. A null scope cannot do\n * that: the query is never issued and the save is never enabled.\n *\n * The rules now deny `hosts/{hostId}/datasets/**` outright, so the path is\n * closed on both sides rather than merely unused on one.\n */\nexport function useOrgDataScope(options: {\n hostId?: string | undefined\n /** An explicit org skips the lookup entirely and is ready immediately. */\n orgId?: string | undefined\n}): OrgDataScope {\n const { hostId, orgId: explicitOrgId } = options\n const state = useHostOrgIdState(explicitOrgId ? undefined : hostId)\n const orgId = explicitOrgId ?? state.orgId\n const loaded = Boolean(explicitOrgId) || state.loaded\n // Memoised because `scope` is a QUERY DEPENDENCY. A fresh tuple every\n // render, passed to `useFirestoreCollection`, tears down and reopens the\n // listener (and clears its data) on every render — so hand out a stable\n // reference rather than trusting every caller to destructure primitives.\n return useMemo(\n () => ({\n orgId,\n ready: loaded,\n scope: orgId ? (['orgs', orgId] as const) : null,\n }),\n [orgId, loaded],\n )\n}\n\nexport default useHostOrgId\n"],"names":["doc","getDoc","useEffect","useMemo","useState","useFirestore","useHostOrgIdState","hostId","firestore","state","setState","orgId","loaded","undefined","active","then","snapshot","data","catch","useHostOrgId","useOrgDataScope","options","explicitOrgId","Boolean","ready","scope"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAChD,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SAASC,YAAY,QAAQ,kCAA8B;AAS3D;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,kBACdC,MAA0B;IAE1B,MAAMC,YAAYH;IAClB,MAAM,CAACI,OAAOC,SAAS,GAAGN,SAAyB;QACjDO,OAAO;QACPC,QAAQ;IACV;IAEAV,UAAU;QACR,sEAAsE;QACtE,+DAA+D;QAC/D,IAAI,CAACK,QAAQ;YACXG,SAAS;gBAAEC,OAAO;gBAAMC,QAAQ;YAAK;YACrC,OAAOC;QACT;QACA,IAAIC,SAAS;QACbJ,SAAS;YAAEC,OAAO;YAAMC,QAAQ;QAAM;QACtC,KAAKX,OAAOD,IAAIQ,WAAW,aAAaD,SACrCQ,IAAI,CAAC,CAACC;YACL,IAAIF,QAAQ;;oBAEAE;gBADVN,SAAS;oBACPC,KAAK,WAAGK,iBAAAA,SAASC,IAAI,uBAAbD,cAAiB,CAAC,QAAQ,mBAAe;oBACjDJ,QAAQ;gBACV;YACF;QACF,GACCM,KAAK,CAAC;YACL,gEAAgE;YAChE,gEAAgE;YAChE,uCAAuC;YACvC,IAAIJ,QAAQJ,SAAS;gBAAEC,OAAO;gBAAMC,QAAQ;YAAK;QACnD;QACF,OAAO;YACLE,SAAS;QACX;IACF,GAAG;QAACN;QAAWD;KAAO;IAEtB,OAAOE;AACT;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASU,aAAaZ,MAA0B;IACrD,OAAOD,kBAAkBC,QAAQI,KAAK;AACxC;AAsBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,OAAO,SAASS,gBAAgBC,OAI/B;IACC,MAAM,EAAEd,MAAM,EAAEI,OAAOW,aAAa,EAAE,GAAGD;IACzC,MAAMZ,QAAQH,kBAAkBgB,gBAAgBT,YAAYN;IAC5D,MAAMI,QAAQW,wBAAAA,gBAAiBb,MAAME,KAAK;IAC1C,MAAMC,SAASW,QAAQD,kBAAkBb,MAAMG,MAAM;IACrD,sEAAsE;IACtE,yEAAyE;IACzE,wEAAwE;IACxE,yEAAyE;IACzE,OAAOT,QACL,IAAO,CAAA;YACLQ;YACAa,OAAOZ;YACPa,OAAOd,QAAS;gBAAC;gBAAQA;aAAM,GAAa;QAC9C,CAAA,GACA;QAACA;QAAOC;KAAO;AAEnB;AAEA,eAAeO,aAAY"}
@@ -0,0 +1,43 @@
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
+ */
17
+ /**
18
+ * Quota-governed host resource kinds served by `POST /api/hosts/resources`
19
+ * (AGL-473). Firestore rules deny client-side `create` on these
20
+ * collections, so creation MUST go through the API — it enforces the
21
+ * per-plan quota (and entitlement, where the feature is paid) with the
22
+ * Admin SDK. Updates and deletes stay client-direct.
23
+ */
24
+ export type HostResourceKind = 'screen' | 'layout' | 'variable' | 'function' | 'workflow' | 'service' | 'redirect' | 'location' | 'product' | 'reusableComponent' | 'form' | 'register' | 'template' | 'webhook' | 'action' | 'entry' | 'author';
25
+ /**
26
+ * Creates a quota-governed host resource through the console API
27
+ * (AGL-473). `data` must be JSON-plain — no Firestore Timestamps; the
28
+ * route stamps `createdAt`/`updatedAt` server-side when absent. Pass `id`
29
+ * when the caller pre-generates the id (e.g. to write a first `versions`
30
+ * doc under it). Pass `parentId` for a nested resource — `entry`, which
31
+ * hangs off `hosts/{hostId}/collections/{parentId}` (AGL-2266). Throws with
32
+ * the server's message on denial (quota, entitlement, role), for the
33
+ * caller's snackbar.
34
+ */
35
+ export declare function useHostResourceApi(): (options: {
36
+ hostId: string;
37
+ resource: HostResourceKind;
38
+ data: Record<string, unknown>;
39
+ id?: string;
40
+ parentId?: string;
41
+ }) => Promise<{
42
+ id: string;
43
+ }>;
@@ -0,0 +1,60 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { useCallback } from "react";
18
+ import { useUser } from "./firebase/firebase-services.js";
19
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
20
+ /**
21
+ * Creates a quota-governed host resource through the console API
22
+ * (AGL-473). `data` must be JSON-plain — no Firestore Timestamps; the
23
+ * route stamps `createdAt`/`updatedAt` server-side when absent. Pass `id`
24
+ * when the caller pre-generates the id (e.g. to write a first `versions`
25
+ * doc under it). Pass `parentId` for a nested resource — `entry`, which
26
+ * hangs off `hosts/{hostId}/collections/{parentId}` (AGL-2266). Throws with
27
+ * the server's message on denial (quota, entitlement, role), for the
28
+ * caller's snackbar.
29
+ */ export function useHostResourceApi() {
30
+ const { data: user } = useUser();
31
+ return useCallback(async ({ hostId, resource, data, id, parentId })=>{
32
+ const response = await authorizedFetch(user, '/api/hosts/resources', {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json'
36
+ },
37
+ body: JSON.stringify(_extends({
38
+ hostId,
39
+ resource,
40
+ data
41
+ }, id ? {
42
+ id
43
+ } : {}, parentId ? {
44
+ parentId
45
+ } : {}))
46
+ });
47
+ const result = await response.json().catch(()=>({}));
48
+ if (!response.ok) {
49
+ var _ref;
50
+ throw new Error((_ref = result == null ? void 0 : result.error) != null ? _ref : 'Create failed');
51
+ }
52
+ return {
53
+ id: String(result.id)
54
+ };
55
+ }, [
56
+ user
57
+ ]);
58
+ }
59
+
60
+ //# sourceMappingURL=use-host-resource-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-host-resource-api.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 { useCallback } from 'react'\nimport { useUser } from './firebase/firebase-services'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\n/**\n * Quota-governed host resource kinds served by `POST /api/hosts/resources`\n * (AGL-473). Firestore rules deny client-side `create` on these\n * collections, so creation MUST go through the API — it enforces the\n * per-plan quota (and entitlement, where the feature is paid) with the\n * Admin SDK. Updates and deletes stay client-direct.\n */\nexport type HostResourceKind =\n | 'screen'\n | 'layout'\n | 'variable'\n | 'function'\n | 'workflow'\n | 'service'\n | 'redirect'\n | 'location'\n | 'product'\n | 'reusableComponent'\n // A form, which rides the `reusableComponents` entitlement and the\n // `formsPerHost` ceiling. The route entry existed before any typed caller\n // did, so creating one from the console was unreachable through this hook.\n | 'form'\n | 'register'\n | 'template'\n | 'webhook'\n // AGL-2266: two client-direct classes with no cap on any plan. `action`\n // is a flat host subcollection; `entry` is the one nested resource here\n // and needs `parentId` (the content collection it belongs to).\n | 'action'\n | 'entry'\n // AGL-2486: custom content authors, a flat host subcollection with a flat\n // platform cap — the `action` shape (create denied, update/delete open).\n | 'author'\n\n/**\n * Creates a quota-governed host resource through the console API\n * (AGL-473). `data` must be JSON-plain — no Firestore Timestamps; the\n * route stamps `createdAt`/`updatedAt` server-side when absent. Pass `id`\n * when the caller pre-generates the id (e.g. to write a first `versions`\n * doc under it). Pass `parentId` for a nested resource — `entry`, which\n * hangs off `hosts/{hostId}/collections/{parentId}` (AGL-2266). Throws with\n * the server's message on denial (quota, entitlement, role), for the\n * caller's snackbar.\n */\nexport function useHostResourceApi(): (options: {\n hostId: string\n resource: HostResourceKind\n data: Record<string, unknown>\n id?: string\n parentId?: string\n}) => Promise<{ id: string }> {\n const { data: user } = useUser()\n return useCallback(\n async ({ hostId, resource, data, id, parentId }) => {\n const response = await authorizedFetch(user, '/api/hosts/resources', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n resource,\n data,\n ...(id ? { id } : {}),\n ...(parentId ? { parentId } : {}),\n }),\n })\n const result = await response.json().catch(() => ({}))\n if (!response.ok) {\n throw new Error(result?.error ?? 'Create failed')\n }\n return { id: String(result.id) }\n },\n [user],\n )\n}\n"],"names":["useCallback","useUser","authorizedFetch","useHostResourceApi","data","user","hostId","resource","id","parentId","response","method","headers","body","JSON","stringify","result","json","catch","ok","Error","error","String"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,QAAO;AACnC,SAASC,OAAO,QAAQ,kCAA8B;AACtD,SAASC,eAAe,QAAQ,2CAA0C;AAoC1E;;;;;;;;;CASC,GACD,OAAO,SAASC;IAOd,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGJ;IACvB,OAAOD,YACL,OAAO,EAAEM,MAAM,EAAEC,QAAQ,EAAEH,IAAI,EAAEI,EAAE,EAAEC,QAAQ,EAAE;QAC7C,MAAMC,WAAW,MAAMR,gBAAgBG,MAAM,wBAAwB;YACnEM,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CC,MAAMC,KAAKC,SAAS,CAAC;gBACnBT;gBACAC;gBACAH;eACII,KAAK;gBAAEA;YAAG,IAAI,CAAC,GACfC,WAAW;gBAAEA;YAAS,IAAI,CAAC;QAEnC;QACA,MAAMO,SAAS,MAAMN,SAASO,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACnD,IAAI,CAACR,SAASS,EAAE,EAAE;;YAChB,MAAM,IAAIC,cAAMJ,0BAAAA,OAAQK,KAAK,mBAAI;QACnC;QACA,OAAO;YAAEb,IAAIc,OAAON,OAAOR,EAAE;QAAE;IACjC,GACA;QAACH;KAAK;AAEV"}
@@ -0,0 +1,44 @@
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
+ */
17
+ import type * as Aglyn from '@aglyn/aglyn';
18
+ import type { DocumentReference } from 'firebase/firestore';
19
+ import { type FirestoreDocOptions } from './firebase/firebase-services';
20
+ /**
21
+ * A saved template (AGL-681), at `hosts/{hostId}/templates/{templateId}`.
22
+ *
23
+ * Unlike screens and layouts there is no version subcollection: a template
24
+ * is inert — nothing renders from it until it is used to create something
25
+ * — so there is no published-versus-draft distinction to keep.
26
+ *
27
+ * Compressed at rest like every other besigner document (AGL-1151). A
28
+ * template is the one kind whose whole content arrives by COPY — "save as
29
+ * template" duplicates a screen's entire tree into a single document with no
30
+ * version to spread it across — so it reaches the 1 MiB ceiling at exactly
31
+ * the size the source page does, and about 1.4x sooner stored as a map.
32
+ */
33
+ export declare const useHostTemplateRef: ({ hostId, templateId, }: {
34
+ hostId: Aglyn.HostUid;
35
+ templateId: string;
36
+ }) => DocumentReference<Aglyn.AglynTemplate>;
37
+ export declare const useHostTemplate: (data: {
38
+ hostId: Aglyn.HostUid;
39
+ templateId: string;
40
+ }, options?: FirestoreDocOptions<Aglyn.AglynTemplate>) => {
41
+ doc: import("./firebase/firebase-services").ObservableStatus<Aglyn.AglynTemplate<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
42
+ setDoc: import("./helpers/use-modify-doc-callback").ModifyDocCallback<Aglyn.AglynTemplate<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
43
+ };
44
+ export default useHostTemplate;
@@ -0,0 +1,82 @@
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
+ */ // 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 { decodeStoredNodes, encodeStoredNodes } 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
+ /**
27
+ * A saved template (AGL-681), at `hosts/{hostId}/templates/{templateId}`.
28
+ *
29
+ * Unlike screens and layouts there is no version subcollection: a template
30
+ * is inert — nothing renders from it until it is used to create something
31
+ * — so there is no published-versus-draft distinction to keep.
32
+ *
33
+ * Compressed at rest like every other besigner document (AGL-1151). A
34
+ * template is the one kind whose whole content arrives by COPY — "save as
35
+ * template" duplicates a screen's entire tree into a single document with no
36
+ * version to spread it across — so it reaches the 1 MiB ceiling at exactly
37
+ * the size the source page does, and about 1.4x sooner stored as a map.
38
+ */ export const useHostTemplateRef = ({ hostId, templateId })=>{
39
+ const firestore = useFirestore();
40
+ const ref = doc(firestore, 'hosts', hostId, 'templates', templateId);
41
+ return ref.withConverter({
42
+ toFirestore (data) {
43
+ // `source` is server-managed and frozen in rules (AGL-666) — stripping
44
+ // it here means an ordinary save can never trip that guard.
45
+ const { $id, source } = data, rest = _object_without_properties_loose(data, [
46
+ "$id",
47
+ "source"
48
+ ]);
49
+ // Only emit `nodes` when the write actually carries them (AGL-1250).
50
+ // The besigner's save writes `placeholders` and `editedAt` alongside
51
+ // the tree through a merging `setDoc`, and the template detail page
52
+ // writes a name on its own — encoding unconditionally would ship an
53
+ // empty map on the second of those and merge it over the design.
54
+ if ((rest == null ? void 0 : rest.nodes) === undefined) return _extends({}, rest, {
55
+ updatedAt: Timestamp.now()
56
+ });
57
+ const nodes = encodeStoredNodes(rest.nodes);
58
+ return _extends({}, rest, nodes ? {
59
+ nodes: Bytes.fromUint8Array(nodes)
60
+ } : {}, {
61
+ updatedAt: Timestamp.now()
62
+ });
63
+ },
64
+ fromFirestore (snapshot, options) {
65
+ if (!snapshot.exists()) return undefined;
66
+ const data = snapshot.data(options);
67
+ if ((data == null ? void 0 : data['nodes']) === undefined) return data;
68
+ // BOTH stored forms, permanently. Every template written before this
69
+ // converter compressed is a plain map and nothing migrates them;
70
+ // `decodeStoredNodes` returns one unchanged.
71
+ return _extends({}, data, {
72
+ nodes: decodeStoredNodes(data['nodes'])
73
+ });
74
+ }
75
+ });
76
+ };
77
+ export const useHostTemplate = (data, options)=>{
78
+ return useDoc(useHostTemplateRef(data), options);
79
+ };
80
+ export default useHostTemplate;
81
+
82
+ //# sourceMappingURL=use-host-template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-host-template.tsx"],"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// 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 { decodeStoredNodes, encodeStoredNodes } from '@aglyn/aglyn'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport type { DocumentReference } from 'firebase/firestore'\nimport { Bytes, doc } from 'firebase/firestore'\nimport {\n useFirestore,\n type FirestoreDocOptions,\n} from './firebase/firebase-services'\nimport useDoc from './helpers/use-doc'\n\n/**\n * A saved template (AGL-681), at `hosts/{hostId}/templates/{templateId}`.\n *\n * Unlike screens and layouts there is no version subcollection: a template\n * is inert — nothing renders from it until it is used to create something\n * — so there is no published-versus-draft distinction to keep.\n *\n * Compressed at rest like every other besigner document (AGL-1151). A\n * template is the one kind whose whole content arrives by COPY — \"save as\n * template\" duplicates a screen's entire tree into a single document with no\n * version to spread it across — so it reaches the 1 MiB ceiling at exactly\n * the size the source page does, and about 1.4x sooner stored as a map.\n */\nexport const useHostTemplateRef = ({\n hostId,\n templateId,\n}: {\n hostId: Aglyn.HostUid\n templateId: string\n}) => {\n const firestore = useFirestore()\n const ref = doc(firestore, 'hosts', hostId, 'templates', templateId)\n return ref.withConverter({\n toFirestore(data: Aglyn.AglynTemplate) {\n // `source` is server-managed and frozen in rules (AGL-666) — stripping\n // it here means an ordinary save can never trip that guard.\n const { $id, source, ...rest } = data as Record<string, unknown> & {\n $id?: string\n source?: unknown\n }\n // Only emit `nodes` when the write actually carries them (AGL-1250).\n // The besigner's save writes `placeholders` and `editedAt` alongside\n // the tree through a merging `setDoc`, and the template detail page\n // writes a name on its own — encoding unconditionally would ship an\n // empty map on the second of those and merge it over the design.\n if (rest?.nodes === undefined) return { ...rest, updatedAt: Timestamp.now() }\n const nodes = encodeStoredNodes(rest.nodes)\n return {\n ...rest,\n ...(nodes ? { nodes: Bytes.fromUint8Array(nodes) } : {}),\n updatedAt: Timestamp.now(),\n }\n },\n fromFirestore(snapshot, options) {\n if (!snapshot.exists()) return undefined\n const data = snapshot.data(options)\n if (data?.['nodes'] === undefined) return data as Aglyn.AglynTemplate\n // BOTH stored forms, permanently. Every template written before this\n // converter compressed is a plain map and nothing migrates them;\n // `decodeStoredNodes` returns one unchanged.\n return {\n ...data,\n nodes: decodeStoredNodes(data['nodes']),\n } as Aglyn.AglynTemplate\n },\n }) as DocumentReference<Aglyn.AglynTemplate>\n}\n\nexport const useHostTemplate = (\n data: { hostId: Aglyn.HostUid; templateId: string },\n options?: FirestoreDocOptions<Aglyn.AglynTemplate>,\n) => {\n return useDoc(useHostTemplateRef(data), options)\n}\n\nexport default useHostTemplate\n"],"names":["decodeStoredNodes","encodeStoredNodes","Timestamp","Bytes","doc","useFirestore","useDoc","useHostTemplateRef","hostId","templateId","firestore","ref","withConverter","toFirestore","data","$id","source","rest","nodes","undefined","updatedAt","now","fromUint8Array","fromFirestore","snapshot","options","exists","useHostTemplate"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,4EAA4E;AAC5E,yEAAyE;AACzE,wEAAwE;;;AAExE,SAASA,iBAAiB,EAAEC,iBAAiB,QAAQ,eAAc;AACnE,SAASC,SAAS,QAAQ,+BAA8B;AAExD,SAASC,KAAK,EAAEC,GAAG,QAAQ,qBAAoB;AAC/C,SACEC,YAAY,QAEP,kCAA8B;AACrC,OAAOC,YAAY,uBAAmB;AAEtC;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,qBAAqB,CAAC,EACjCC,MAAM,EACNC,UAAU,EAIX;IACC,MAAMC,YAAYL;IAClB,MAAMM,MAAMP,IAAIM,WAAW,SAASF,QAAQ,aAAaC;IACzD,OAAOE,IAAIC,aAAa,CAAC;QACvBC,aAAYC,IAAyB;YACnC,uEAAuE;YACvE,4DAA4D;YAC5D,MAAM,EAAEC,GAAG,EAAEC,MAAM,EAAW,GAAGF,MAATG,wCAASH;;;;YAIjC,qEAAqE;YACrE,qEAAqE;YACrE,oEAAoE;YACpE,oEAAoE;YACpE,iEAAiE;YACjE,IAAIG,CAAAA,wBAAAA,KAAMC,KAAK,MAAKC,WAAW,OAAO,aAAKF;gBAAMG,WAAWlB,UAAUmB,GAAG;;YACzE,MAAMH,QAAQjB,kBAAkBgB,KAAKC,KAAK;YAC1C,OAAO,aACFD,MACCC,QAAQ;gBAAEA,OAAOf,MAAMmB,cAAc,CAACJ;YAAO,IAAI,CAAC;gBACtDE,WAAWlB,UAAUmB,GAAG;;QAE5B;QACAE,eAAcC,QAAQ,EAAEC,OAAO;YAC7B,IAAI,CAACD,SAASE,MAAM,IAAI,OAAOP;YAC/B,MAAML,OAAOU,SAASV,IAAI,CAACW;YAC3B,IAAIX,CAAAA,wBAAAA,IAAM,CAAC,QAAQ,MAAKK,WAAW,OAAOL;YAC1C,qEAAqE;YACrE,iEAAiE;YACjE,6CAA6C;YAC7C,OAAO,aACFA;gBACHI,OAAOlB,kBAAkBc,IAAI,CAAC,QAAQ;;QAE1C;IACF;AACF,EAAC;AAED,OAAO,MAAMa,kBAAkB,CAC7Bb,MACAW;IAEA,OAAOnB,OAAOC,mBAAmBO,OAAOW;AAC1C,EAAC;AAED,eAAeE,gBAAe"}