@flamingo-stack/openframe-frontend-core 0.0.510 → 0.0.511-snapshot.20260804140004
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-protocol/decode.d.ts.map +1 -1
- package/dist/chat-protocol/frames.d.ts +26 -1
- package/dist/chat-protocol/frames.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +24 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +24 -0
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
- package/dist/chunk-27OKD7ZV.js.map +1 -0
- package/dist/{chunk-RHQ7BJKK.cjs → chunk-2WWDZR4D.cjs} +73 -73
- package/dist/{chunk-RHQ7BJKK.cjs.map → chunk-2WWDZR4D.cjs.map} +1 -1
- package/dist/{chunk-J2JUAS4C.cjs → chunk-2XL7UK5Y.cjs} +11 -11
- package/dist/{chunk-J2JUAS4C.cjs.map → chunk-2XL7UK5Y.cjs.map} +1 -1
- package/dist/{chunk-Q4K3TGNP.cjs → chunk-3NECEXDF.cjs} +74 -44
- package/dist/chunk-3NECEXDF.cjs.map +1 -0
- package/dist/{chunk-JOCFPAS6.js → chunk-4NFDSMB5.js} +6 -6
- package/dist/{chunk-SYZFFRVD.cjs → chunk-5BAGE7AI.cjs} +13 -13
- package/dist/{chunk-SYZFFRVD.cjs.map → chunk-5BAGE7AI.cjs.map} +1 -1
- package/dist/{chunk-53JKCFFE.cjs → chunk-5JBDT2MQ.cjs} +12 -12
- package/dist/{chunk-53JKCFFE.cjs.map → chunk-5JBDT2MQ.cjs.map} +1 -1
- package/dist/{chunk-XJVRAKEX.cjs → chunk-67YDSTH7.cjs} +56 -33
- package/dist/chunk-67YDSTH7.cjs.map +1 -0
- package/dist/{chunk-WNOGR5AA.js → chunk-6LMTBNLN.js} +3 -3
- package/dist/{chunk-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
- package/dist/{chunk-JMBIGH4H.cjs → chunk-7VBEOGCR.cjs} +83 -83
- package/dist/{chunk-JMBIGH4H.cjs.map → chunk-7VBEOGCR.cjs.map} +1 -1
- package/dist/{chunk-5AZZQQYS.cjs → chunk-AWLFJMOJ.cjs} +40 -40
- package/dist/{chunk-5AZZQQYS.cjs.map → chunk-AWLFJMOJ.cjs.map} +1 -1
- package/dist/{chunk-RJ3D3PE2.cjs → chunk-DQDPDUI3.cjs} +9 -9
- package/dist/{chunk-RJ3D3PE2.cjs.map → chunk-DQDPDUI3.cjs.map} +1 -1
- package/dist/{chunk-2RLI5XVI.cjs → chunk-F2YI5N7E.cjs} +16 -16
- package/dist/{chunk-2RLI5XVI.cjs.map → chunk-F2YI5N7E.cjs.map} +1 -1
- package/dist/{chunk-LBOWSKGM.js → chunk-FI35LJ3L.js} +5 -5
- package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
- package/dist/chunk-FS3YSNSU.js.map +1 -0
- package/dist/{chunk-RB2EW6IZ.cjs → chunk-GOFTC2HV.cjs} +3690 -3472
- package/dist/chunk-GOFTC2HV.cjs.map +1 -0
- package/dist/{chunk-CTMLY2GN.js → chunk-HGZPH7S5.js} +5 -5
- package/dist/{chunk-7VUKIWF6.cjs → chunk-HIV2GIFP.cjs} +3 -3
- package/dist/{chunk-7VUKIWF6.cjs.map → chunk-HIV2GIFP.cjs.map} +1 -1
- package/dist/{chunk-DBR5JLX3.js → chunk-IHNUMRMU.js} +46 -23
- package/dist/chunk-IHNUMRMU.js.map +1 -0
- package/dist/{chunk-R4HKB3YD.cjs → chunk-IIPBTWQR.cjs} +22 -22
- package/dist/{chunk-R4HKB3YD.cjs.map → chunk-IIPBTWQR.cjs.map} +1 -1
- package/dist/{chunk-TBGSQTJK.js → chunk-IK2WRYVA.js} +6 -6
- package/dist/{chunk-WSAPKKDU.cjs → chunk-JIW5JI3E.cjs} +9 -9
- package/dist/{chunk-WSAPKKDU.cjs.map → chunk-JIW5JI3E.cjs.map} +1 -1
- package/dist/{chunk-MLGGF24T.js → chunk-JKAX5K7C.js} +8 -8
- package/dist/{chunk-UWPT23SA.js → chunk-JO3NPEK6.js} +3 -3
- package/dist/{chunk-HCZJZMU7.js → chunk-JOFGJYIT.js} +3 -3
- package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
- package/dist/chunk-KI4EGKNV.cjs.map +1 -0
- package/dist/{chunk-OB7YDOTK.cjs → chunk-NJADHL2G.cjs} +8 -8
- package/dist/{chunk-OB7YDOTK.cjs.map → chunk-NJADHL2G.cjs.map} +1 -1
- package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
- package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
- package/dist/{chunk-HX4CJYXL.js → chunk-OT4RV6WY.js} +10 -10
- package/dist/{chunk-Z35QWWJH.cjs → chunk-RJXI25TJ.cjs} +34 -34
- package/dist/{chunk-Z35QWWJH.cjs.map → chunk-RJXI25TJ.cjs.map} +1 -1
- package/dist/{chunk-6TL5XIAW.cjs → chunk-SG25J6LV.cjs} +119 -119
- package/dist/{chunk-6TL5XIAW.cjs.map → chunk-SG25J6LV.cjs.map} +1 -1
- package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
- package/dist/{chunk-FEGEC6FY.js → chunk-V5WLIFKN.js} +5660 -5442
- package/dist/chunk-V5WLIFKN.js.map +1 -0
- package/dist/{chunk-EJ3EBYNV.cjs → chunk-VVXUUBML.cjs} +32 -32
- package/dist/{chunk-EJ3EBYNV.cjs.map → chunk-VVXUUBML.cjs.map} +1 -1
- package/dist/{chunk-SRYV4H3M.js → chunk-VZX3SSFL.js} +3 -3
- package/dist/{chunk-5CDPRCPS.cjs → chunk-WVZHCDB5.cjs} +23 -23
- package/dist/{chunk-5CDPRCPS.cjs.map → chunk-WVZHCDB5.cjs.map} +1 -1
- package/dist/{chunk-VDJF4ZAU.js → chunk-XG7FLAKQ.js} +3 -3
- package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
- package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
- package/dist/{chunk-2AASLY6D.js → chunk-ZIQMVAGA.js} +3 -3
- package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/calendar.js +3 -3
- package/dist/components/case-studies/index.cjs +17 -17
- package/dist/components/case-studies/index.js +7 -7
- package/dist/components/chat/approval-batch-message.d.ts +2 -2
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/deleted-data-card.d.ts +27 -0
- package/dist/components/chat/entity-cards/deleted-data-card.d.ts.map +1 -0
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-card-item.d.ts +3 -0
- package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +13 -7
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +12 -6
- package/dist/components/chat/remark-mention-chips.d.ts +5 -2
- package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
- package/dist/components/chat/remark-strip-citations.d.ts +22 -0
- package/dist/components/chat/remark-strip-citations.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/stream/message-mutations.d.ts +49 -5
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +7 -7
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +11 -4
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +4 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts +48 -0
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts.map +1 -0
- package/dist/components/chat/utils/index.d.ts +2 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -3
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +63 -0
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -0
- package/dist/components/chat/utils/source-row-cta.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +10 -10
- package/dist/components/docs/index.js +9 -9
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/embeds/index.js +7 -7
- package/dist/components/faq/index.cjs +9 -9
- package/dist/components/faq/index.js +8 -8
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/features/index.js +6 -6
- package/dist/components/help-center-pages/index.cjs +28 -28
- package/dist/components/help-center-pages/index.js +19 -19
- package/dist/components/icons/index.cjs +3 -3
- package/dist/components/icons/index.js +2 -2
- package/dist/components/index.cjs +253 -247
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -17
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +92 -92
- package/dist/components/meeting-scheduler/index.js +9 -9
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +11 -11
- package/dist/components/onboarding-guides/index.js +10 -10
- package/dist/components/related-content/index.cjs +9 -9
- package/dist/components/related-content/index.js +8 -8
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +11 -3
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -1
- package/dist/components/shared/doc-search/use-doc-search.d.ts +7 -0
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +11 -11
- package/dist/components/tickets/index.js +10 -10
- package/dist/components/ui/actions-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +70 -70
- package/dist/components/ui/file-manager/index.js +3 -3
- package/dist/components/ui/index.cjs +13 -7
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +12 -6
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/overlay-open-registry.d.ts +50 -0
- package/dist/components/ui/overlay-open-registry.d.ts.map +1 -0
- package/dist/components/ui/portal-container.d.ts +24 -0
- package/dist/components/ui/portal-container.d.ts.map +1 -1
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/index.js +3 -3
- package/dist/index.cjs +20 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -14
- package/dist/utils/anchor-id.d.ts +32 -0
- package/dist/utils/anchor-id.d.ts.map +1 -0
- package/dist/utils/content-href.d.ts +27 -6
- package/dist/utils/content-href.d.ts.map +1 -1
- package/dist/utils/index.cjs +213 -152
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +212 -153
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/same-page-hash-nav.d.ts +17 -6
- package/dist/utils/same-page-hash-nav.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/chat-protocol/decode.ts +43 -0
- package/src/chat-protocol/frames.ts +27 -0
- package/src/components/chat/__tests__/remark-mention-chips.test.ts +67 -0
- package/src/components/chat/approval-batch-message.tsx +2 -2
- package/src/components/chat/chat-message-enhanced.tsx +5 -3
- package/src/components/chat/chat-message-list.tsx +49 -5
- package/src/components/chat/embeddable-chat.tsx +19 -2
- package/src/components/chat/entity-cards/deleted-data-card.tsx +61 -0
- package/src/components/chat/entity-cards/dispatch.tsx +205 -102
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +1 -1
- package/src/components/chat/hooks/use-chat-card-item.ts +15 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +28 -4
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +57 -32
- package/src/components/chat/remark-mention-chips.ts +6 -3
- package/src/components/chat/remark-strip-citations.ts +39 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-approval-batch.test.ts +196 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +67 -2
- package/src/components/chat/stream/message-mutations.ts +170 -40
- package/src/components/chat/types/component.types.ts +9 -7
- package/src/components/chat/types/message.types.ts +14 -4
- package/src/components/chat/types/unified-chat-state.types.ts +4 -1
- package/src/components/chat/utils/__tests__/discuss-ref-prompt.test.ts +67 -0
- package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +156 -0
- package/src/components/chat/utils/discuss-ref-prompt.ts +63 -0
- package/src/components/chat/utils/index.ts +16 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +24 -29
- package/src/components/chat/utils/resolve-fetched-card-href.ts +104 -0
- package/src/components/chat/utils/source-row-cta.ts +5 -1
- package/src/components/shared/doc-search/__tests__/resolve-search-result-action.test.ts +145 -0
- package/src/components/shared/doc-search/resolve-search-result-action.ts +46 -10
- package/src/components/shared/doc-search/use-doc-search.ts +50 -14
- package/src/components/ui/__tests__/overlay-open-registry.test.tsx +87 -0
- package/src/components/ui/actions-menu.tsx +65 -2
- package/src/components/ui/dropdown-menu.tsx +62 -12
- package/src/components/ui/markdown/base-components.tsx +26 -0
- package/src/components/ui/overlay-open-registry.tsx +99 -0
- package/src/components/ui/portal-container.tsx +31 -0
- package/src/utils/__tests__/anchor-id.test.ts +139 -0
- package/src/utils/__tests__/content-href.test.ts +74 -0
- package/src/utils/__tests__/list-url.test.ts +11 -1
- package/src/utils/anchor-id.ts +106 -0
- package/src/utils/content-href.ts +113 -13
- package/src/utils/index.ts +8 -0
- package/src/utils/list-url.ts +6 -1
- package/src/utils/same-page-hash-nav.ts +28 -9
- package/dist/chunk-A62C4C4V.cjs.map +0 -1
- package/dist/chunk-BTUHMCYW.js.map +0 -1
- package/dist/chunk-DBR5JLX3.js.map +0 -1
- package/dist/chunk-EYTXD62B.cjs.map +0 -1
- package/dist/chunk-FEGEC6FY.js.map +0 -1
- package/dist/chunk-GE3JUTW2.js.map +0 -1
- package/dist/chunk-Q4K3TGNP.cjs.map +0 -1
- package/dist/chunk-RB2EW6IZ.cjs.map +0 -1
- package/dist/chunk-XJVRAKEX.cjs.map +0 -1
- /package/dist/{chunk-JOCFPAS6.js.map → chunk-4NFDSMB5.js.map} +0 -0
- /package/dist/{chunk-WNOGR5AA.js.map → chunk-6LMTBNLN.js.map} +0 -0
- /package/dist/{chunk-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
- /package/dist/{chunk-LBOWSKGM.js.map → chunk-FI35LJ3L.js.map} +0 -0
- /package/dist/{chunk-CTMLY2GN.js.map → chunk-HGZPH7S5.js.map} +0 -0
- /package/dist/{chunk-TBGSQTJK.js.map → chunk-IK2WRYVA.js.map} +0 -0
- /package/dist/{chunk-MLGGF24T.js.map → chunk-JKAX5K7C.js.map} +0 -0
- /package/dist/{chunk-UWPT23SA.js.map → chunk-JO3NPEK6.js.map} +0 -0
- /package/dist/{chunk-HCZJZMU7.js.map → chunk-JOFGJYIT.js.map} +0 -0
- /package/dist/{chunk-HX4CJYXL.js.map → chunk-OT4RV6WY.js.map} +0 -0
- /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
- /package/dist/{chunk-SRYV4H3M.js.map → chunk-VZX3SSFL.js.map} +0 -0
- /package/dist/{chunk-VDJF4ZAU.js.map → chunk-XG7FLAKQ.js.map} +0 -0
- /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
- /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
- /package/dist/{chunk-2AASLY6D.js.map → chunk-ZIQMVAGA.js.map} +0 -0
- /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
* })
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { canonicalContentRefType } from './list-url'
|
|
30
|
+
import { byKey, getPlatformProductionUrl } from '../platform-domains'
|
|
31
|
+
|
|
29
32
|
/**
|
|
30
33
|
* Type → in-app route suffix. The public-hostable subset of the hub's
|
|
31
34
|
* `PUBLIC_URL_PATHS` (`lib/utils/content-url-builder.ts`); the hub keeps
|
|
@@ -56,14 +59,32 @@ export const DEFAULT_CONTENT_SUFFIXES: Record<string, string> = {
|
|
|
56
59
|
* `identifier` (the primary-key id) + `externalUrl` (the canonical hub URL,
|
|
57
60
|
* from which the slug is recovered for in-app routing). */
|
|
58
61
|
export interface ComposeContentUrlInput {
|
|
59
|
-
/** The content's documentType (e.g. `'product_release'`,
|
|
62
|
+
/** The content's CANONICAL documentType (e.g. `'product_release'`,
|
|
63
|
+
* `'blog_post'`).
|
|
64
|
+
*
|
|
65
|
+
* Rail-vocab aliases (`'blog_post_existing'`) must be canonicalized by the
|
|
66
|
+
* caller via `canonicalContentRefType` BEFORE they reach the seam: a host
|
|
67
|
+
* writes its `hostedTypes` / `suffixes` / `overrides` against the canonical
|
|
68
|
+
* vocabulary, so an alias silently misses every one of them and falls
|
|
69
|
+
* through to the default `<contentOrigin>/<type>/<id>` branch — which is how
|
|
70
|
+
* the same blog post reached `/blog/<slug>` from one path and
|
|
71
|
+
* `/blog_post_existing/<slug>` from another. */
|
|
60
72
|
type: string
|
|
61
73
|
/** Content identifier. Page views pass the slug; chat rows pass the
|
|
62
74
|
* primary-key id (the slug is recovered from `externalUrl` when hosted). */
|
|
63
75
|
identifier: string
|
|
64
|
-
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
76
|
+
/** Preferred path segment for HOSTED types when there is no `externalUrl`
|
|
77
|
+
* to recover the slug from — the Mingo/NATS transport ships bare
|
|
78
|
+
* `[card://type:id]` markers with no ref metadata, so a fetch-mode card
|
|
79
|
+
* knows the row's slug only AFTER it loads the row. `identifier` must stay
|
|
80
|
+
* the primary key (that is what `overrides` deep-link on), hence a separate
|
|
81
|
+
* field rather than overloading it. Ignored for non-hosted types. */
|
|
82
|
+
slug?: string | null
|
|
83
|
+
/** Hydrated platform junction from the list APIs. `hostedTypes` membership
|
|
84
|
+
* still decides in-app vs out; this decides WHICH origin an out-link points
|
|
85
|
+
* at — an OpenMSP-owned row resolves to openmsp.ai, not to the embedder's
|
|
86
|
+
* `contentOrigin`. Read only when `targetPlatform` is absent. Accepts the
|
|
87
|
+
* three junction shapes the DALs produce (see `primaryPlatformOf`). */
|
|
67
88
|
platforms?: Array<{ name?: string }>
|
|
68
89
|
/** The canonical hub URL when the caller already has it (chat entity rows
|
|
69
90
|
* carry it from the RAG mapper). Hosted types relativize it to an in-app
|
|
@@ -78,8 +99,11 @@ export interface ContentHrefOptions {
|
|
|
78
99
|
/** Types THIS host serves in-app → relative href (soft-nav). Everything
|
|
79
100
|
* else resolves to the row's `externalUrl` / `contentOrigin` (opens out). */
|
|
80
101
|
hostedTypes: ReadonlySet<string>
|
|
81
|
-
/**
|
|
82
|
-
* `https://openframe.app`).
|
|
102
|
+
/** Fallback origin for non-hosted types with no `externalUrl` AND no
|
|
103
|
+
* resolvable owning platform (e.g. `https://openframe.app`). When the row
|
|
104
|
+
* DOES name its platform, that platform's canonical origin wins — otherwise
|
|
105
|
+
* cross-platform content (an OpenMSP blog post) would be linked to the
|
|
106
|
+
* wrong site. */
|
|
83
107
|
contentOrigin: string
|
|
84
108
|
/** Per-type route suffix. Defaults to {@link DEFAULT_CONTENT_SUFFIXES}. */
|
|
85
109
|
suffixes?: Record<string, string>
|
|
@@ -95,6 +119,35 @@ export type ComposeContentUrl = (
|
|
|
95
119
|
input: ComposeContentUrlInput,
|
|
96
120
|
) => { href: string; targetPlatform: string | null }
|
|
97
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Owning platform of a content row, from the hydrated junction the list APIs
|
|
124
|
+
* return. Mirrors the hub's `extractPrimaryPlatform` (`lib/utils/content-url-
|
|
125
|
+
* builder.ts`): the same junction arrives in three shapes depending on which
|
|
126
|
+
* DAL produced it, so all three are read.
|
|
127
|
+
*
|
|
128
|
+
* 1. `{ platforms: { name } }` — raw Supabase row (rag-mappers)
|
|
129
|
+
* 2. `{ platform_name }` — legacy pre-flattened admin endpoints
|
|
130
|
+
* 3. `{ name }` — modern flattened shape (blog / case-study utils)
|
|
131
|
+
*
|
|
132
|
+
* Returns the FIRST resolvable name; `null` when the junction is absent or
|
|
133
|
+
* carries no usable platform (caller then falls back to `contentOrigin`).
|
|
134
|
+
*/
|
|
135
|
+
function primaryPlatformOf(
|
|
136
|
+
platforms: ComposeContentUrlInput['platforms'],
|
|
137
|
+
): string | null {
|
|
138
|
+
if (!Array.isArray(platforms)) return null
|
|
139
|
+
for (const entry of platforms) {
|
|
140
|
+
const row = entry as {
|
|
141
|
+
name?: unknown
|
|
142
|
+
platform_name?: unknown
|
|
143
|
+
platforms?: { name?: unknown }
|
|
144
|
+
}
|
|
145
|
+
const name = row?.platforms?.name ?? row?.platform_name ?? row?.name
|
|
146
|
+
if (typeof name === 'string' && name.trim()) return name.trim()
|
|
147
|
+
}
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
98
151
|
/** Last non-empty path segment of a URL (relative or absolute) — the content
|
|
99
152
|
* slug of a canonical detail URL like `https://hub/releases/my-release`.
|
|
100
153
|
* Returns `null` on a malformed URL or empty path. */
|
|
@@ -127,7 +180,19 @@ function lastPathSegment(url: string): string | null {
|
|
|
127
180
|
*/
|
|
128
181
|
export function makeComposeContentUrl(opts: ContentHrefOptions): ComposeContentUrl {
|
|
129
182
|
const suffixes = opts.suffixes ?? DEFAULT_CONTENT_SUFFIXES
|
|
130
|
-
return ({
|
|
183
|
+
return ({
|
|
184
|
+
type: rawType,
|
|
185
|
+
identifier,
|
|
186
|
+
slug: slugHint,
|
|
187
|
+
externalUrl,
|
|
188
|
+
targetPlatform,
|
|
189
|
+
platforms,
|
|
190
|
+
}) => {
|
|
191
|
+
// Defence in depth: callers are expected to hand over a canonical
|
|
192
|
+
// documentType (see `ComposeContentUrlInput.type`), but a stray alias must
|
|
193
|
+
// not silently produce a `<origin>/blog_post_existing/<slug>` URL. Same
|
|
194
|
+
// canonicalizer `buildListUrl` uses — one alias table, both paths.
|
|
195
|
+
const type = canonicalContentRefType(rawType)
|
|
131
196
|
const override =
|
|
132
197
|
opts.overrides && Object.prototype.hasOwnProperty.call(opts.overrides, type)
|
|
133
198
|
? opts.overrides[type]
|
|
@@ -143,15 +208,50 @@ export function makeComposeContentUrl(opts: ContentHrefOptions): ComposeContentU
|
|
|
143
208
|
// segment is the type's OWN suffix (e.g. a malformed/list externalUrl like
|
|
144
209
|
// `https://hub/releases/` → `'releases'`), fall back to `identifier` instead
|
|
145
210
|
// of emitting a nonsensical `/releases/releases`.
|
|
211
|
+
//
|
|
212
|
+
// `slug` (the explicit hint) sits between the two: a Mingo fetch-mode card
|
|
213
|
+
// has NO externalUrl to recover from but DOES know the row's slug once it
|
|
214
|
+
// loads, and its `identifier` must stay the primary key so `overrides`
|
|
215
|
+
// still deep-link correctly.
|
|
146
216
|
const recovered = externalUrl ? lastPathSegment(externalUrl) : null
|
|
147
|
-
const slug =
|
|
217
|
+
const slug =
|
|
218
|
+
recovered && recovered !== seg ? recovered : (slugHint?.trim() || identifier)
|
|
148
219
|
return { href: `/${seg}/${slug}`, targetPlatform: null }
|
|
149
220
|
}
|
|
150
|
-
// Not hosted → opens out. Prefer the RAG-authoritative `externalUrl` (chat)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
221
|
+
// Not hosted → opens out. Prefer the RAG-authoritative `externalUrl` (chat).
|
|
222
|
+
if (externalUrl) return { href: externalUrl, targetPlatform: targetPlatform ?? null }
|
|
223
|
+
|
|
224
|
+
// No externalUrl → compose it. The destination is the origin of the
|
|
225
|
+
// platform that OWNS the row, not a fixed hub: an OpenMSP blog post lives
|
|
226
|
+
// on openmsp.ai, and pinning every non-hosted type to `contentOrigin` sent
|
|
227
|
+
// it to flamingo.run (a 404 / wrong site). Owner comes from the explicit
|
|
228
|
+
// `targetPlatform`, else the hydrated platforms junction — mirroring the
|
|
229
|
+
// hub's `composeContentUrlFromPlatforms` + `extractPrimaryPlatform`.
|
|
230
|
+
// Origins resolve through the platform-domain SSOT (env override → default
|
|
231
|
+
// production URL), so a per-deploy `NEXT_PUBLIC_*_URL` is honoured here too.
|
|
232
|
+
//
|
|
233
|
+
// `contentOrigin` stays the fallback for rows with no resolvable owner and
|
|
234
|
+
// for platform names the registry doesn't know — an embedder's explicit
|
|
235
|
+
// configuration must win over the registry's flamingo default.
|
|
236
|
+
// Only a REGISTRY-RESOLVED owner counts. An unknown name (a platform the
|
|
237
|
+
// registry doesn't carry) falls back to `contentOrigin` for the href, and
|
|
238
|
+
// reporting it as the `targetPlatform` anyway made the two disagree:
|
|
239
|
+
// `decideNewTab` prefers `targetPlatform` over its origin check, so a link
|
|
240
|
+
// pointing at the CURRENT host opened in a new tab.
|
|
241
|
+
const claimedOwner = targetPlatform ?? primaryPlatformOf(platforms)
|
|
242
|
+
const owner = claimedOwner && byKey(claimedOwner) ? claimedOwner : null
|
|
243
|
+
const origin = owner ? getPlatformProductionUrl(owner) : opts.contentOrigin
|
|
244
|
+
// The hub's public detail routes are SLUG-based, so the `slug` hint wins
|
|
245
|
+
// over `identifier` here too — a Mingo fetch-mode card knows the row's slug
|
|
246
|
+
// but its `identifier` is the marker's primary key, which would 404.
|
|
247
|
+
return {
|
|
248
|
+
href: `${origin.replace(/\/+$/, '')}/${seg}/${slugHint?.trim() || identifier}`,
|
|
249
|
+
// Report the owner so `decideNewTab` can compare it against the current
|
|
250
|
+
// app instead of falling back to an origin guess (which is unreliable in
|
|
251
|
+
// dev, where every platform shares localhost). `null` whenever the href
|
|
252
|
+
// came from `contentOrigin`, so the two never disagree.
|
|
253
|
+
targetPlatform: owner,
|
|
254
|
+
}
|
|
155
255
|
}
|
|
156
256
|
}
|
|
157
257
|
|
package/src/utils/index.ts
CHANGED
|
@@ -299,6 +299,14 @@ export {
|
|
|
299
299
|
type NavigateSamePageHashOptions,
|
|
300
300
|
} from './same-page-hash-nav'
|
|
301
301
|
|
|
302
|
+
// GitHub-compatible anchor resolution. Doc TOCs are authored against
|
|
303
|
+
// GitHub's slugger (`## 📚 Table of Contents` → `#-table-of-contents`)
|
|
304
|
+
// while our heading ids trim the emoji's leftover hyphen — this bridges
|
|
305
|
+
// that without changing the id shape the "On this page" rail and every
|
|
306
|
+
// shared URL already depend on. Consumers resolving a hash want
|
|
307
|
+
// `getHashTargetElement` (above), which applies this as its last pass.
|
|
308
|
+
export { findAnchorElementByNormalizedId, normalizeAnchorId } from './anchor-id'
|
|
309
|
+
|
|
302
310
|
// Shared list-API URL builder — the single source for the per-type chat
|
|
303
311
|
// entity-card fetch shapes. The hub's 12 RAG mapper `listApi` closures
|
|
304
312
|
// delegate here (byte-parity test guards the migration); embedders wire
|
package/src/utils/list-url.ts
CHANGED
|
@@ -43,7 +43,12 @@ const ALIASES: Record<string, string> = { blog_post_existing: 'blog_post' }
|
|
|
43
43
|
* registry entityTypes (e.g. the related-content rail's same-type-first
|
|
44
44
|
* ordering) share THIS alias map instead of re-declaring it. */
|
|
45
45
|
export function canonicalContentRefType(contentRefType: string): string {
|
|
46
|
-
|
|
46
|
+
// `hasOwnProperty` guard, same as `buildListUrl` below: a bare index read
|
|
47
|
+
// resolves prototype keys, so `canonicalContentRefType('constructor')`
|
|
48
|
+
// returned `Object` itself — which then got interpolated into a URL path.
|
|
49
|
+
return Object.prototype.hasOwnProperty.call(ALIASES, contentRefType)
|
|
50
|
+
? ALIASES[contentRefType]
|
|
51
|
+
: contentRefType
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { scrollElementIntoView } from './scroll-into-view'
|
|
2
|
+
import { findAnchorElementByNormalizedId } from './anchor-id'
|
|
2
3
|
|
|
3
4
|
/** Pages with a section-nav STRIP on top of the global hub header
|
|
4
5
|
* (dev-center roadmap/delivery/tickets, FAQ category-pill nav).
|
|
@@ -24,24 +25,42 @@ export function normalizeHashFragment(hash: string): string {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
* Resolve the DOM element a hash fragment id points at
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* Resolve the DOM element a hash fragment id points at — THE hash resolver;
|
|
29
|
+
* `navigateSamePageHash`, `useScrollToHash` and the markdown link renderer
|
|
30
|
+
* all go through this one, so a fragment that resolves for a deep link
|
|
31
|
+
* resolves for an in-body click too.
|
|
32
|
+
*
|
|
33
|
+
* Three passes, cheapest first:
|
|
34
|
+
* 1. the raw id (ordinary anchors — the overwhelming common case);
|
|
35
|
+
* 2. the percent-decoded form — href composers like `buildTicketOpenHref`
|
|
36
|
+
* encode the id into the fragment while rows render the raw id, so
|
|
37
|
+
* `#ticket-a%2Fb` must still find `id="ticket-a/b"`. A malformed escape
|
|
38
|
+
* sequence just falls through;
|
|
39
|
+
* 3. a NORMALIZED match against the document's headings — doc bodies are
|
|
40
|
+
* authored on GitHub, whose slugger keeps the hyphen left behind by a
|
|
41
|
+
* stripped emoji (`## 📚 Table of Contents` → `#-table-of-contents`)
|
|
42
|
+
* while our ids trim it. Without this every in-body TOC link in every
|
|
43
|
+
* GitHub-authored doc resolves to nothing and clicking one does nothing.
|
|
44
|
+
* See `utils/anchor-id`.
|
|
33
45
|
*/
|
|
34
46
|
export function getHashTargetElement(id: string): HTMLElement | null {
|
|
35
47
|
if (!id) return null
|
|
36
48
|
const direct = document.getElementById(id)
|
|
37
49
|
if (direct) return direct
|
|
50
|
+
|
|
51
|
+
let decoded = id
|
|
38
52
|
try {
|
|
39
|
-
|
|
40
|
-
if (decoded !== id)
|
|
53
|
+
decoded = decodeURIComponent(id)
|
|
54
|
+
if (decoded !== id) {
|
|
55
|
+
const decodedEl = document.getElementById(decoded)
|
|
56
|
+
if (decodedEl) return decodedEl
|
|
57
|
+
}
|
|
41
58
|
} catch {
|
|
42
59
|
// malformed escape — the raw lookup above already covered it
|
|
60
|
+
decoded = id
|
|
43
61
|
}
|
|
44
|
-
|
|
62
|
+
|
|
63
|
+
return findAnchorElementByNormalizedId(decoded, document)
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
export interface NavigateSamePageHashOptions {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-A62C4C4V.cjs","../src/platform-domains.ts","../src/utils/cn.ts"],"names":[],"mappings":"AAAA,6rBAAY;AACZ;AACE;AACF,wDAA6B;AAC7B;AACA;AC4EO,SAAS,KAAA,CAAM,GAAA,EAA8C;AAClE,EAAA,OAAO,gBAAA,CAAiB,IAAA,CAAK,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,IAAA,IAAQ,GAAG,CAAA;AACnD;AAGA,SAAS,cAAA,CAAe,GAAA,EAA4B;AAClD,EAAA,MAAM,OAAA,kBAAS,KAAA,iBAAM,GAAG,CAAA,6BAAG,QAAA;AAC3B,EAAA,GAAA,CAAI,CAAC,MAAA,EAAQ,OAAO,IAAA;AACpB,EAAA,OAAQ,aAAA,CAAqD,MAAM,EAAA,GAAK,IAAA;AAC1E;AAiBO,SAAS,YAAA,CAAa,GAAA,EAAqB;AAChD,EAAA,MAAM,QAAA,EAAU,GAAA,CAAI,IAAA,CAAK,CAAA;AACzB,EAAA,GAAA,CAAI,0BAAA,CAA2B,IAAA,CAAK,OAAO,CAAA,EAAG,OAAO,OAAA;AACrD,EAAA,OAAO,CAAA,QAAA,EAAW,OAAA,CAAQ,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,CAAA;AAC/C;AASmE;AAG/D,EAAA;AAG0B,EAAA;AAC9B;AAKwE;AACnD,EAAA;AACf,EAAA;AACyC,IAAA;AACrC,EAAA;AACC,IAAA;AACT,EAAA;AACF;AAGsD;AACT,EAAA;AACD,EAAA;AAC9B,EAAA;AACd;AAG0E;AAC5C,EAAA;AACkB,EAAA;AACvC,EAAA;AACT;AAGoD;AACZ,EAAA;AACxC;AAGgG;AACtE,EAAA;AACe,EAAA;AACA,EAAA;AAChC,EAAA;AACT;AAOkF;AAC9C,EAAA;AACI,EAAA;AAClB,IAAA;AACyB,IAAA;AAC7C,EAAA;AACO,EAAA;AACT;AAWyD;AACjB,EAAA;AACxC;AAgBsD;AACT,EAAA;AAEV,EAAA;AAGY,EAAA;AACnC,IAAA;AACV,EAAA;AAI6B,EAAA;AAGb,EAAA;AACqB,IAAA;AACrC,EAAA;AAGoC,EAAA;AACE,EAAA;AAClB,IAAA;AACE,IAAA;AACT,IAAA;AAC8B,IAAA;AAC/B,IAAA;AACkB,MAAA;AACN,MAAA;AACtB,IAAA;AACF,EAAA;AAC6B,EAAA;AAC/B;AA6CM;AA9RN;AAAA,EAAA;AAAA,IAAA;AAkCgE,IAAA;AAC1B,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAAuE;AAAA;AAAA;AAAA;AAKpE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACG,MAAA;AACH,MAAA;AACzC,IAAA;AAgBsB,IAAA;AACmB,MAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACD,MAAA;AACH,MAAA;AACC,MAAA;AACJ,MAAA;AACK,MAAA;AACF,MAAA;AACvC,IAAA;AAgN6D,IAAA;AAEzB,IAAA;AAM1B,MAAA;AACN,QAAA;AAEF,MAAA;AACF,IAAA;AAAA,EAAA;AAAA;AD5KgD;AACA;AE/HV;AACF;AA4CQ;AACf,EAAA;AAC7B;AAsBsD;AAET,EAAA;AACC,IAAA;AAC5C,EAAA;AAGc,EAAA;AAC4B,IAAA;AAC1C,EAAA;AAGgB,EAAA;AACgB,IAAA;AAChC,EAAA;AAK4C,EAAA;AAC9C;AAhEM;AAzBN;AAAA,EAAA;AAAA,IAAA;AAMA,IAAA;AAmBsD,IAAA;AAC5C,MAAA;AACO,QAAA;AACO,UAAA;AAChB,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACF,UAAA;AACF,QAAA;AACF,MAAA;AACD,IAAA;AAAA,EAAA;AAAA;AFgI+C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-A62C4C4V.cjs","sourcesContent":[null,"/**\n * Platform-Domain SSOT (single source of truth) + derivations.\n *\n * ONE registry maps each platform → its canonical production URL (`defaultUrl`,\n * the load-bearing source) with an optional per-deploy `NEXT_PUBLIC_*_URL`\n * OVERRIDE. Everything else — the reverse host→platform resolver, the cookie\n * base-domain set (the cross-subdomain SSO mechanism), www/apex expansion, the\n * URL→host parse, preview detection — derives from this one table.\n *\n * EDGE-SAFE + PURE: no React/clsx/tailwind, no `server-only`, no `node:`\n * builtins, no `'use client'`. So it is legal in the Edge middleware\n * (`proxy.ts`), in `'use client'` providers, AND in `server-only` modules\n * (e.g. cookie-domain-server.ts) simultaneously. The ONLY non-pure export is\n * `getAllPlatformBaseDomains`, which reads `typeof window`/`process.env` to\n * preserve byte-identical cookie behavior.\n */\n\nimport type { PlatformName } from './types/platform'\n\nexport type PlatformDomainKey = PlatformName | 'openframe-dashboard'\n\nexport interface PlatformDomainEntry {\n /** Platform key (matches `PlatformName`, plus the forward-only `openframe-dashboard`). */\n key: PlatformDomainKey\n /** Canonical production URL — the LOAD-BEARING source of truth (today's hardcoded fallbacks). */\n defaultUrl: string\n /** `NEXT_PUBLIC_*_URL` per-deploy OVERRIDE. The `defaultUrl` covers it when unset. */\n envVar: string\n /** Legacy/secondary hosts that REVERSE-map to this key (no env var exists — NOT canonical). */\n aliasHostnames?: string[]\n /** Forward-only: no DB row, excluded from the reverse index + cookie set (e.g. the product-CTA dashboard). */\n pseudo?: boolean\n}\n\nexport const PLATFORM_DOMAINS: readonly PlatformDomainEntry[] = [\n { key: 'marketing-hub', defaultUrl: 'https://marketing-hub.flamingo.so', envVar: 'NEXT_PUBLIC_MARKETING_HUB_URL' },\n { key: 'company-hub', defaultUrl: 'https://company-hub.flamingo.so', envVar: 'NEXT_PUBLIC_COMPANY_HUB_URL' },\n { key: 'product-hub', defaultUrl: 'https://product-hub.flamingo.so', envVar: 'NEXT_PUBLIC_PRODUCT_HUB_URL' },\n { key: 'revenue-hub', defaultUrl: 'https://revenue-hub.flamingo.so', envVar: 'NEXT_PUBLIC_REVENUE_HUB_URL' },\n { key: 'people-hub', defaultUrl: 'https://people-hub.flamingo.so', envVar: 'NEXT_PUBLIC_PEOPLE_HUB_URL' },\n { key: 'openmsp', defaultUrl: 'https://www.openmsp.ai', envVar: 'NEXT_PUBLIC_OPENMSP_URL' },\n // ORDERING INVARIANT (first-wins, load-bearing): `flamingo` MUST precede `flamingo-teaser` + `universal`.\n // All three resolve to www.flamingo.run; the reverse index is first-wins → flamingo claims the shared host,\n // teaser keeps only its unique flamingo.cx aliases, universal contributes no unique host.\n // ⚠️ DO NOT REORDER — enforced by the module-load self-check at the bottom of this file.\n { key: 'flamingo', defaultUrl: 'https://www.flamingo.run', envVar: 'NEXT_PUBLIC_FLAMINGO_URL' },\n { key: 'tmcg', defaultUrl: 'https://www.tmcg.miami', envVar: 'NEXT_PUBLIC_TMCG_URL' },\n { key: 'flamingo-teaser', defaultUrl: 'https://www.flamingo.run', envVar: 'NEXT_PUBLIC_FLAMINGO_URL', aliasHostnames: ['flamingo.cx', 'www.flamingo.cx'] },\n { key: 'openframe', defaultUrl: 'https://openframe.ai', envVar: 'NEXT_PUBLIC_OPENFRAME_URL', aliasHostnames: ['openframe.ai', 'www.openframe.ai', 'hub.openframe.ai'] },\n { key: 'openframe-dashboard', defaultUrl: 'https://openframe.ai', envVar: 'NEXT_PUBLIC_OPENFRAME_DASHBOARD_URL', pseudo: true },\n { key: 'universal', defaultUrl: 'https://www.flamingo.run', envVar: 'NEXT_PUBLIC_FLAMINGO_URL' },\n]\n\n// ── Compile-time key guards (anchor the table on PlatformName, both directions) ──\n// (1) Removal/typo guard: every table key must be a valid PlatformDomainKey.\nconst _tableSatisfies = PLATFORM_DOMAINS satisfies readonly PlatformDomainEntry[]\nvoid _tableSatisfies\n// (2) Addition guard: a NEW PlatformName member that lacks a table row fails the build.\ntype _MissingKey = Exclude<PlatformName, (typeof PLATFORM_DOMAINS)[number]['key']>\nconst _exhaustive: [_MissingKey] extends [never] ? true : false = true\nvoid _exhaustive\n\n// ── Env overrides (the ONLY place env URLs enter — literal-key inlined + compile-time-guarded) ──\n// `process.env.NEXT_PUBLIC_X` is build-inlined ONLY with a LITERAL key, so the env-var name is the\n// irreducible two-copy (the registry `envVar` column + the literal access below). The `satisfies` makes\n// `tsc`/`next build` FAIL if this map is missing a registry env var OR carries a stale one (bidirectional).\ntype EnvVarKey = (typeof PLATFORM_DOMAINS)[number]['envVar']\nconst ENV_OVERRIDES = {\n NEXT_PUBLIC_MARKETING_HUB_URL: process.env.NEXT_PUBLIC_MARKETING_HUB_URL,\n NEXT_PUBLIC_COMPANY_HUB_URL: process.env.NEXT_PUBLIC_COMPANY_HUB_URL,\n NEXT_PUBLIC_PRODUCT_HUB_URL: process.env.NEXT_PUBLIC_PRODUCT_HUB_URL,\n NEXT_PUBLIC_REVENUE_HUB_URL: process.env.NEXT_PUBLIC_REVENUE_HUB_URL,\n NEXT_PUBLIC_PEOPLE_HUB_URL: process.env.NEXT_PUBLIC_PEOPLE_HUB_URL,\n NEXT_PUBLIC_OPENMSP_URL: process.env.NEXT_PUBLIC_OPENMSP_URL,\n NEXT_PUBLIC_FLAMINGO_URL: process.env.NEXT_PUBLIC_FLAMINGO_URL,\n NEXT_PUBLIC_TMCG_URL: process.env.NEXT_PUBLIC_TMCG_URL,\n NEXT_PUBLIC_OPENFRAME_URL: process.env.NEXT_PUBLIC_OPENFRAME_URL,\n NEXT_PUBLIC_OPENFRAME_DASHBOARD_URL: process.env.NEXT_PUBLIC_OPENFRAME_DASHBOARD_URL,\n} satisfies Record<EnvVarKey, string | undefined>\n\n/** The registry entry for a key (undefined for an unknown key). */\nexport function byKey(key: string): PlatformDomainEntry | undefined {\n return PLATFORM_DOMAINS.find((e) => e.key === key)\n}\n\n/** Read a platform's `NEXT_PUBLIC_*_URL` override (or null). */\nfunction envOverrideFor(key: string): string | null {\n const envVar = byKey(key)?.envVar\n if (!envVar) return null\n return (ENV_OVERRIDES as Record<string, string | undefined>)[envVar] || null\n}\n\n/**\n * Ensure a URL string carries a scheme. Per-deploy `NEXT_PUBLIC_*_URL` overrides are\n * stored SCHEME-LESS (bare host, e.g. `www.openmsp.ai` / `hub.openframe.ai`) — the\n * canonical convention in the Vercel shared-env store. This normalizes them to a full\n * `https://` URL so every downstream consumer (`hostOf`/`new URL`, hrefs, the cookie\n * base-domain derivation, CSP) receives a parseable URL. Full-URL inputs (the registry\n * `defaultUrl`s, any scheme'd override) pass through unchanged.\n *\n * EXPORTED as the single owner of the scheme-normalization rule (next.config.mjs keeps a\n * byte-identical local copy ONLY because Next evaluates its config outside the TS module\n * graph and cannot import this — see the comment there).\n *\n * Handles a (theoretical) protocol-relative `//host` too: strips the leading slashes so it\n * doesn't become `https:////host` (empty-host → hostOf null → silent platform drop).\n */\nexport function ensureScheme(url: string): string {\n const trimmed = url.trim()\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(trimmed)) return trimmed // already has a scheme\n return `https://${trimmed.replace(/^\\/+/, '')}` // bare host or protocol-relative `//host`\n}\n\n/**\n * Canonical production URL for a platform: env override wins, else the `defaultUrl`.\n * NEVER throws / undefined — the default guarantees a host (this is what keeps the\n * cookie base-domains, the reverse map, and CSP intact even with every override unset).\n * The result ALWAYS carries a scheme (`ensureScheme`), so the scheme-less env overrides\n * resolve to valid URLs. Unknown-key fallback preserves cn.ts's flamingo.run default.\n */\nexport function getPlatformProductionUrl(platform: string): string {\n const resolved =\n envOverrideFor(platform) ??\n byKey(platform)?.defaultUrl ??\n envOverrideFor('flamingo') ??\n 'https://www.flamingo.run'\n return ensureScheme(resolved)\n}\n\n// ── Single-owner host primitives ──\n\n/** Canonical URL→host parser: `.hostname` (PORT-STRIPPED, lowercased), null on parse failure. */\nexport function hostOf(value: string | null | undefined): string | null {\n if (!value) return null\n try {\n return new URL(value).hostname.toLowerCase()\n } catch {\n return null\n }\n}\n\n/** Expand a host into its `www.`/apex pair. 3+-label and single-label hosts return `[host]`. */\nexport function expandWwwApex(host: string): string[] {\n if (host.startsWith('www.')) return [host, host.slice(4)]\n if (host.split('.').length === 2) return [host, `www.${host}`]\n return [host]\n}\n\n/** Registrable base domain (`parts.slice(-2).join('.')`), dotless; undefined for <2-label. */\nexport function toRegistrableBaseDomain(host: string): string | undefined {\n const parts = host.split('.')\n if (parts.length >= 2) return parts.slice(-2).join('.')\n return undefined\n}\n\n/** An entry's alias hosts (single-owner reader). */\nexport function aliasHostsOf(key: string): string[] {\n return byKey(key)?.aliasHostnames ?? []\n}\n\n/** All hosts an entry contributes to the reverse index (resolved host + optional aliases). */\nfunction hostsForEntry(entry: PlatformDomainEntry, opts: { includeAliases: boolean }): string[] {\n const resolved = hostOf(getPlatformProductionUrl(entry.key))\n const hosts = resolved ? expandWwwApex(resolved) : []\n if (opts.includeAliases) hosts.push(...aliasHostsOf(entry.key))\n return hosts\n}\n\n/**\n * Reverse resolver: hostname → platform key (first-wins over registry order, non-pseudo only).\n * Guarantees openframe.ai / www.openframe.ai / hub.openframe.ai → openframe in every env.\n * Replaces the hub `PLATFORM_DOMAIN_MAP`.\n */\nexport function getPlatformByHostname(hostname: string): PlatformDomainKey | null {\n const host = hostname.toLowerCase()\n for (const entry of PLATFORM_DOMAINS) {\n if (entry.pseudo) continue\n if (hostsForEntry(entry, { includeAliases: true }).includes(host)) return entry.key\n }\n return null\n}\n\n// ── Preview detection (two distinct predicates — env-form vs host-form) ──\n\n/** Env-form preview predicate (Vercel `VERCEL_ENV`). */\nexport function isPreviewEnv(): boolean {\n return process.env.VERCEL_ENV === 'preview'\n}\n\n/** Host-form preview predicate (a `*.vercel.app` host). Dot-bounded suffix so a\n * malicious `foo.vercel.app.evil.com` is NOT treated as preview. */\nexport function isPreviewHost(hostname: string): boolean {\n return hostname.endsWith('.vercel.app')\n}\n\n/**\n * ALL unique cookie base domains (the cross-subdomain SSO mechanism).\n *\n * NON-PURE (the sole such export): reads `typeof window` + `process.env` to\n * preserve byte-identical cookie behavior. Keeps the original three branches:\n * 1. localhost / private IP → [] (host-only cookies)\n * 2. Vercel preview (env OR host) → ['.vercel.app','vercel.app']\n * 3. production → for each non-pseudo platform, registrable base of its resolved\n * host, emitted as both `.base` and bare `base`.\n *\n * Because `getPlatformProductionUrl` always yields a host (override OR default),\n * `.flamingo.so` / `.flamingo.run` / `.openmsp.ai` / `.tmcg.miami` / `.openframe.ai`\n * are ALWAYS present → cross-hub SSO is byte-identical to today.\n */\nexport function getAllPlatformBaseDomains(): string[] {\n if (typeof window === 'undefined') return []\n\n const hostname = window.location.hostname\n\n // Case 1: localhost / private IP — no domains\n if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('127.')) {\n return []\n }\n\n // Case 2: Vercel preview — vercel.app domain\n const previewEnv =\n process.env.VERCEL_ENV === 'preview' ||\n process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' ||\n isPreviewHost(hostname)\n if (previewEnv) {\n return ['.vercel.app', 'vercel.app']\n }\n\n // Case 3: production — registrable base of every non-pseudo platform's resolved host\n const baseDomains = new Set<string>()\n for (const entry of PLATFORM_DOMAINS) {\n if (entry.pseudo) continue\n const host = hostOf(getPlatformProductionUrl(entry.key))\n if (!host) continue\n const base = toRegistrableBaseDomain(host)\n if (base) {\n baseDomains.add(`.${base}`)\n baseDomains.add(base)\n }\n }\n return Array.from(baseDomains)\n}\n\n// ── Cookie-domain guard + match (single owner; shared by the client cookie-domain.ts +\n// server-only cookie-domain-server.ts resolvers, which previously hand-rolled both 3×) ──\n\n/**\n * Hosts that must NOT receive a `Domain=` cookie → the caller returns undefined (host-only):\n * localhost, loopback/private IPs, and any `*.vercel.app`. `vercel.app` is on the Public Suffix\n * List, so browsers SILENTLY drop `Set-Cookie: Domain=.vercel.app` — which broke the PKCE verifier\n * + session cookies on preview deploys. Host-only is sufficient there (the same preview host\n * round-trips the OAuth chain); production hosts (`.flamingo.so`/`.openmsp.ai`/…) fall through.\n */\nexport function isNonCookieableHost(hostname: string): boolean {\n return (\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname.startsWith('127.') ||\n hostname.startsWith('192.168.') ||\n hostname.startsWith('10.') ||\n hostname.includes('.vercel.app')\n )\n}\n\n/**\n * Match a hostname against a set of registrable base domains → the dotted cookie `Domain`\n * (`.flamingo.so`), or undefined when none contains the host. Accepts bases with or without a\n * leading dot and always returns the dotted form. Single owner for the match loop both resolvers ran.\n */\nexport function matchCookieDomain(hostname: string, baseDomains: string[]): string | undefined {\n for (const domain of baseDomains) {\n const bare = domain.startsWith('.') ? domain.slice(1) : domain\n if (hostname === bare || hostname.endsWith(`.${bare}`)) {\n return domain.startsWith('.') ? domain : `.${domain}`\n }\n }\n return undefined\n}\n\n// ── Module-load ordering self-check (NON-fatal — this module is imported by cn.ts → ~everything,\n// so a hard throw would be a total outage if the assertion were ever over-strict). Checks the REAL\n// invariant — table order — STRUCTURALLY (findIndex), NOT via getPlatformByHostname on a hardcoded\n// host: the latter false-positives when NEXT_PUBLIC_FLAMINGO_URL is overridden to a non-default host\n// (flamingo's resolved host changes, so `www.flamingo.run` no longer reverse-maps to it though the\n// ordering is fine). The alias checks below ARE env-immune (aliasHostnames are unique per key). The\n// authoritative guard is the reverse-map vitest. ⚠️ keep `flamingo` before `flamingo-teaser`/`universal`.\nconst _orderIdx = (k: PlatformDomainKey) => PLATFORM_DOMAINS.findIndex((e) => e.key === k)\nif (\n _orderIdx('flamingo') > _orderIdx('flamingo-teaser') ||\n _orderIdx('flamingo') > _orderIdx('universal') ||\n getPlatformByHostname('flamingo.cx') !== 'flamingo-teaser' ||\n getPlatformByHostname('hub.openframe.ai') !== 'openframe'\n) {\n // eslint-disable-next-line no-console\n console.error(\n '[platform-domains] ⚠️ PLATFORM_DOMAINS ordering invariant violated — `flamingo` must precede ' +\n '`flamingo-teaser`/`universal`, and the openframe/teaser aliases must be intact. Do not reorder the table.',\n )\n}\n","import { clsx, type ClassValue } from \"clsx\"\nimport { extendTailwindMerge } from \"tailwind-merge\"\n// Platform→domain resolution moved to the SSOT module `src/platform-domains.ts`.\n// `getPlatformProductionUrl` / `getAllPlatformBaseDomains` now live there (re-exported\n// via the utils barrel for existing callers); `getBaseUrl` stays here because it owns the\n// dev-localhost + Vercel-self-origin branches, and delegates its platform branch.\nimport { getPlatformProductionUrl } from \"../platform-domains\"\n\n/**\n * EVERY custom `text-*` utility we add in `tailwind.config.ts` MUST be listed here.\n *\n * tailwind-merge has no knowledge of our utilities, and `text-<word>` is\n * indistinguishable from a text COLOUR to it. So an unlisted `text-badge` gets\n * filed in tailwind-merge's `text-color` group, where it and any real colour\n * class silently annihilate each other — last one wins, the other is dropped\n * from the output entirely:\n *\n * cn('text-badge', 'text-ods-text-on-accent') -> 'text-ods-text-on-accent' (size lost)\n * cn('text-[--some-colour]', 'text-badge') -> 'text-badge' (colour lost)\n *\n * Listing them in their own group makes them non-conflicting with colours, so\n * both survive. This is why `text-h1`…`text-h6` are here; `text-code` and\n * `text-badge` were added to the config later and missing them shipped exactly\n * the two failures above (huge badges, and badge text losing its colour).\n */\nconst twMerge = extendTailwindMerge<'ods-typography'>({\n extend: {\n classGroups: {\n 'ods-typography': [\n 'text-h1',\n 'text-h2',\n 'text-h3',\n 'text-h4',\n 'text-h5',\n 'text-h6',\n 'text-code',\n 'text-badge',\n ],\n },\n },\n})\n\n/**\n * Combine class names with Tailwind's merge utility\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n\n/**\n * Get the application base URL for the current environment\n *\n * @param platform - Optional platform name (openmsp, flamingo, tmcg, openframe, etc.)\n * @returns The base URL with protocol (https:// or http://)\n *\n * Priority order:\n * 1. Development (http://localhost:3000)\n * 2. Platform-specific URL via the SSOT (if `platform` provided) — env override ?? default\n * 3. VERCEL_PROJECT_PRODUCTION_URL (Vercel production domain)\n * 4. Production fallback: the DEPLOYING platform's canonical URL (NEXT_PUBLIC_APP_TYPE),\n * openmsp if unset — sourced from the SSOT, no hardcoded literal.\n *\n * The per-platform canonical URLs + their `NEXT_PUBLIC_*_URL` overrides are the single\n * source of truth in `src/platform-domains.ts` (`PLATFORM_DOMAINS`).\n *\n * @example\n * getBaseUrl() // Current deployment's URL\n * getBaseUrl('flamingo') // https://www.flamingo.run (prod) or http://localhost:3000 (dev)\n */\nexport function getBaseUrl(platform?: string): string {\n // In development, always use localhost (regardless of platform)\n if (process.env.NODE_ENV !== 'production') {\n return process.env.NEXT_PUBLIC_DEV_URL || 'http://localhost:3000'\n }\n\n // If platform is specified, return its production URL (env override ?? default)\n if (platform) {\n return getPlatformProductionUrl(platform)\n }\n\n // Production: Use Vercel domain if available\n if (process.env.VERCEL_PROJECT_PRODUCTION_URL) {\n return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`\n }\n\n // Production fallback: the deploying platform's canonical www domain (avoids Google\n // \"Page with redirect\"). Derived from the SSOT for the current app type (openmsp when\n // unset → 'https://www.openmsp.ai', byte-identical to the old hardcoded fallback).\n return getPlatformProductionUrl(process.env.NEXT_PUBLIC_APP_TYPE || 'openmsp')\n}\n"]}
|